@momo-kits/native-kits 0.167.1-duo.7 → 0.167.1-duo.8-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CLAUDE.md +7 -4
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +204 -0
  4. package/compose/build.gradle.kts.backup +204 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +708 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +203 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +92 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +366 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  136. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  137. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  138. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  139. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
  140. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  141. package/gradle/libs.versions.toml +69 -0
  142. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  143. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  144. package/gradle.properties +26 -0
  145. package/gradlew +252 -0
  146. package/gradlew.bat +94 -0
  147. package/ios/Application/Layout/ScreenLayout.swift +122 -6
  148. package/ios/Application/Navigation/BottomTab/BottomTab.swift +41 -5
  149. package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  150. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +60 -31
  151. package/ios/Application/Navigation/component/DuoScrollGradient.swift +18 -19
  152. package/ios/Application/Navigation/component/Header.swift +3 -2
  153. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +32 -0
  154. package/ios/Application/Navigation/component/SideBar.swift +2 -2
  155. package/ios/Application/StackScreen.swift +46 -6
  156. package/ios-demo/README.md +120 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +7 -3
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +3 -0
  159. package/package.json +1 -1
  160. package/settings.gradle.kts +64 -0
@@ -0,0 +1,708 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.Animatable
5
+ import androidx.compose.animation.core.AnimationVector1D
6
+ import androidx.compose.animation.core.tween
7
+ import androidx.compose.foundation.ScrollState
8
+ import androidx.compose.foundation.background
9
+ import androidx.compose.foundation.border
10
+ import androidx.compose.foundation.gestures.ScrollableState
11
+ import androidx.compose.foundation.gestures.animateScrollBy
12
+ import androidx.compose.foundation.layout.Box
13
+ import androidx.compose.foundation.layout.Column
14
+ import androidx.compose.foundation.layout.ColumnScope
15
+ import androidx.compose.foundation.layout.Spacer
16
+ import androidx.compose.foundation.layout.WindowInsets
17
+ import androidx.compose.foundation.layout.asPaddingValues
18
+ import androidx.compose.foundation.layout.aspectRatio
19
+ import androidx.compose.foundation.layout.fillMaxSize
20
+ import androidx.compose.foundation.layout.fillMaxWidth
21
+ import androidx.compose.foundation.layout.height
22
+ import androidx.compose.foundation.layout.ime
23
+ import androidx.compose.foundation.layout.imePadding
24
+ import androidx.compose.foundation.layout.offset
25
+ import androidx.compose.foundation.layout.padding
26
+ import androidx.compose.foundation.lazy.LazyListState
27
+ import androidx.compose.foundation.rememberScrollState
28
+ import androidx.compose.foundation.verticalScroll
29
+ import androidx.compose.material.ExperimentalMaterialApi
30
+ import androidx.compose.runtime.Composable
31
+ import androidx.compose.runtime.CompositionLocalProvider
32
+ import androidx.compose.runtime.LaunchedEffect
33
+ import androidx.compose.runtime.State
34
+ import androidx.compose.runtime.getValue
35
+ import androidx.compose.runtime.mutableIntStateOf
36
+ import androidx.compose.runtime.mutableStateOf
37
+ import androidx.compose.runtime.remember
38
+ import androidx.compose.runtime.rememberUpdatedState
39
+ import androidx.compose.runtime.snapshotFlow
40
+ import androidx.compose.runtime.staticCompositionLocalOf
41
+ import androidx.compose.ui.Alignment
42
+ import androidx.compose.ui.Modifier
43
+ import androidx.compose.ui.graphics.Brush
44
+ import androidx.compose.ui.graphics.Color
45
+ import androidx.compose.ui.graphics.graphicsLayer
46
+ import androidx.compose.ui.graphics.lerp
47
+ import androidx.compose.ui.layout.onGloballyPositioned
48
+ import androidx.compose.ui.platform.LocalDensity
49
+ import androidx.compose.ui.unit.Dp
50
+ import androidx.compose.ui.unit.dp
51
+ import androidx.compose.ui.unit.lerp
52
+ import androidx.compose.ui.zIndex
53
+ import kotlinx.coroutines.flow.Flow
54
+ import kotlinx.coroutines.flow.filter
55
+ import kotlinx.coroutines.launch
56
+ import vn.momo.kits.components.InputSearch
57
+ import vn.momo.kits.const.AppNavigationBar
58
+ import vn.momo.kits.const.AppTheme
59
+ import vn.momo.kits.application.IsShowBaseLineDebug
60
+ import vn.momo.kits.const.Colors
61
+ import vn.momo.kits.const.Spacing
62
+ import vn.momo.kits.modifier.conditional
63
+ import vn.momo.kits.modifier.hideKeyboardOnTap
64
+ import vn.momo.kits.navigation.bottomtab.BOTTOM_TAB_BAR_HEIGHT
65
+ import vn.momo.kits.navigation.component.FABPosition
66
+ import vn.momo.kits.navigation.component.FloatingButton
67
+ import vn.momo.kits.navigation.component.HEADER_HEIGHT
68
+ import vn.momo.kits.navigation.component.Header
69
+ import vn.momo.kits.navigation.component.HeaderBackground
70
+ import vn.momo.kits.navigation.component.HeaderRight
71
+ import vn.momo.kits.navigation.component.HeaderSideBar
72
+ import vn.momo.kits.navigation.bottomtab.LocalDuoRailReserve
73
+ import vn.momo.kits.navigation.bottomtab.duoFloatingTabBarInset
74
+ import vn.momo.kits.navigation.component.HeaderType
75
+ import vn.momo.kits.navigation.component.InputSearchType
76
+ import vn.momo.kits.platform.BackHandler
77
+ import vn.momo.kits.platform.supportsImePadding
78
+ import vn.momo.kits.navigation.tracking.ScreenTracker
79
+ import vn.momo.kits.navigation.tracking.ScreenTrackingState
80
+ import kotlinx.coroutines.delay
81
+ import vn.momo.kits.application.LocalContext
82
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
83
+ import vn.momo.kits.layout.LocalScreenLayout
84
+ import vn.momo.kits.layout.ScreenLayout
85
+ import vn.momo.kits.layout.consumeHorizontalInsets
86
+ import vn.momo.kits.layout.getScreenLayout
87
+ import kotlin.time.Clock
88
+ import kotlin.time.Duration.Companion.milliseconds
89
+ import kotlin.time.ExperimentalTime
90
+
91
+ internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
92
+ internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
93
+
94
+ // Space a tab's content leaves for the tab bar and home indicator. On iPhone Duo the floating bar
95
+ // overlays the content and the rail lives in the side strip, so only their own inset applies.
96
+ @Composable
97
+ private fun bottomTabChildInset(): Dp {
98
+ val keyboard = keyboardSizeState().value
99
+ val navigationBar = AppNavigationBar.current
100
+ val layout = getScreenLayout()
101
+ return when {
102
+ layout.isIPhoneDuo && layout.sideBar == null -> (duoFloatingTabBarInset(layout) - keyboard).coerceAtLeast(0.dp)
103
+ layout.isIPhoneDuo -> (navigationBar - keyboard).coerceAtLeast(0.dp)
104
+ else -> (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp) +
105
+ (navigationBar - keyboard).coerceAtLeast(0.dp)
106
+ }
107
+ }
108
+
109
+ @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
110
+ @Composable
111
+ internal fun StackScreen(
112
+ content: @Composable () -> Unit,
113
+ navigationOptions: NavigationOptions? = null,
114
+ id: Int = -1,
115
+ name: String = "",
116
+ bottomTabIndex: Int = -1,
117
+ onBackHandler: (() -> Unit)? = null,
118
+ ) {
119
+ val navigator = LocalNavigator.current
120
+ val maxApi = LocalMaxApi.current
121
+ val context = LocalContext.current
122
+ val screenLayout = getScreenLayout()
123
+ val density = LocalDensity.current
124
+ val screenRegistry = LocalDynamicScreenRegistry.current
125
+ val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
126
+
127
+ val options by navigation.currentOptions
128
+ val isBottomTabChild = bottomTabIndex != -1
129
+ val isBottomTabRoot = navigation.isBottomTabRoot
130
+
131
+ // Auto tracking state
132
+ val trackingState = remember {
133
+ ScreenTrackingState().apply {
134
+ time = Clock.System.now().toEpochMilliseconds()
135
+ }
136
+ }
137
+
138
+ // Determine action: push or back
139
+ val action = remember {
140
+ if (ScreenTracker.getLastScreenName() != null) "push" else "back"
141
+ }
142
+
143
+ // Auto tracking effects
144
+ LaunchedEffect(name) {
145
+ // Track screen navigated immediately
146
+ ScreenTracker.trackScreenNavigated(
147
+ maxApi = maxApi,
148
+ context = context,
149
+ screenName = name,
150
+ action = action,
151
+ state = trackingState
152
+ )
153
+
154
+ // Track screen displayed after 2 seconds (debounce)
155
+ delay(2000.milliseconds)
156
+ val loadTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
157
+ ScreenTracker.trackScreenDisplayed(
158
+ maxApi = maxApi,
159
+ context = context,
160
+ screenName = name,
161
+ duration = loadTime,
162
+ state = trackingState
163
+ )
164
+
165
+ // Track screen interacted after displayed
166
+ val interactionTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
167
+ ScreenTracker.trackScreenInteracted(
168
+ maxApi = maxApi,
169
+ context = context,
170
+ screenName = name,
171
+ duration = interactionTime - loadTime,
172
+ totalDuration = interactionTime,
173
+ state = trackingState
174
+ )
175
+ }
176
+
177
+ val limit = with(density) {
178
+ screenLayout.headerHeight.toPx()
179
+ }.toInt()
180
+
181
+ val lazyListState = options.scrollData.scrollState as? LazyListState
182
+ val (scrollState, _) = if (lazyListState != null)
183
+ lazyListState.proxyScrollState(limit, 1)
184
+ else
185
+ (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 1)
186
+ val snapState: ScrollableState = lazyListState ?: scrollState
187
+ val headerHide = rememberQuickReturnHide(
188
+ enabled = options.usesDuoQuickReturnHeader(screenLayout),
189
+ scrollState = scrollState,
190
+ heightPx = with(density) { screenLayout.headerHeight.toPx() },
191
+ )
192
+
193
+ val footerHeightPx = remember { mutableIntStateOf(0) }
194
+ val headerRightWidthPx = remember { mutableIntStateOf(0) }
195
+
196
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
197
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
198
+ }
199
+
200
+ val canPop = navigator.canPop()
201
+
202
+ val hasCustomBackHandling = !SwipeBackConfig.enabled ||
203
+ OverplayComponentRegistry.isOverplayShowing() ||
204
+ !canPop
205
+
206
+ BackHandler(hasCustomBackHandling) { navigator.onBackSafe() }
207
+
208
+ CompositionLocalProvider(
209
+ StackScreenScrollableState provides options.scrollData.scrollState,
210
+ LocalNavigation provides navigation,
211
+ LocalScrollState provides scrollState,
212
+ LocalOptions provides options,
213
+ LocalFooterHeightPx provides footerHeightPx,
214
+ LocalHeaderRightWidthPx provides headerRightWidthPx
215
+ ) {
216
+ Box(
217
+ Modifier
218
+ .fillMaxSize()
219
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
220
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
221
+ hideKeyboardOnTap()
222
+ }
223
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
224
+ imePadding()
225
+ }
226
+ ) {
227
+ Box(Modifier.zIndex(1f)) {
228
+ HeaderBackground()
229
+ }
230
+
231
+ Box(Modifier.zIndex(4f).graphicsLayer { translationY = -headerHide.value }) {
232
+ Header(onBackHandler)
233
+ }
234
+
235
+ HeaderSideBar(
236
+ bottomInset = maxOf(
237
+ bottomBarsHeight(isBottomTabChild) + Spacing.XL,
238
+ if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
239
+ ),
240
+ modifier = Modifier.zIndex(4f),
241
+ )
242
+
243
+ Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
244
+ SearchAnimated(snapState = snapState)
245
+ }
246
+
247
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
248
+ MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
249
+ FooterContent()
250
+ }
251
+
252
+ Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
253
+ FloatingContent(isBottomTabChild = isBottomTabChild)
254
+ }
255
+
256
+ OverplayView(isBottomTabChild = isBottomTabChild, id = id)
257
+ }
258
+ }
259
+ }
260
+
261
+ // Height the footer, tab bar or home indicator takes from the bottom of the screen.
262
+ @Composable
263
+ private fun bottomBarsHeight(isBottomTabChild: Boolean): Dp {
264
+ val options = LocalOptions.current
265
+ val navigationBar = AppNavigationBar.current
266
+ return when {
267
+ options.hasFooter() -> with(LocalDensity.current) { LocalFooterHeightPx.current.intValue.toDp() }
268
+ isBottomTabChild -> bottomTabChildInset()
269
+ else -> navigationBar
270
+ }
271
+ }
272
+
273
+ @Suppress("FrequentlyChangingValue")
274
+ @Composable
275
+ internal fun FloatingContent(isBottomTabChild: Boolean) {
276
+ val options = LocalOptions.current
277
+ val scrollState = LocalScrollState.current
278
+
279
+ val fabProps = options.floatingButtonProps ?: return
280
+ val bottomPadding = fabProps.bottom ?: (Spacing.M + bottomBarsHeight(isBottomTabChild))
281
+
282
+ FloatingButton(
283
+ scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
284
+ onClick = fabProps.onClick,
285
+ containerColor = AppTheme.current.colors.primary,
286
+ bottom = bottomPadding,
287
+ icon = fabProps.icon,
288
+ iconColor = fabProps.iconColor,
289
+ text = fabProps.label,
290
+ size = fabProps.size,
291
+ position = fabProps.position ?: FABPosition.END,
292
+ )
293
+ }
294
+
295
+ @Composable
296
+ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTabRoot: Boolean, isBottomTabChild: Boolean) {
297
+ val options = LocalOptions.current
298
+ val inputSearchType = getInputSearchType(options)
299
+ val density = LocalDensity.current
300
+ val scrollState = LocalScrollState.current
301
+ val navigationBar = AppNavigationBar.current
302
+ val screenLayout = getScreenLayout()
303
+ val useDuoGlass = options.usesDuoGlassHeader(screenLayout)
304
+
305
+ Spacer(
306
+ Modifier.height(
307
+ if (!useDuoGlass && (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent)))
308
+ screenLayout.headerHeight else 0.dp
309
+ )
310
+ )
311
+ if (inputSearchType == InputSearchType.Animated) {
312
+ val scrollDp = with(density) { scrollState.value.toDp() }
313
+
314
+ Spacer(Modifier.height((HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp)))
315
+ }
316
+ // a bottom tab root stays full width: the tab bar and each tab's own StackScreen handle the insets
317
+ val insetContent = !isBottomTabRoot
318
+ Column(
319
+ Modifier
320
+ .fillMaxWidth()
321
+ .weight(1f)
322
+ .conditional(insetContent && options.headerType !is HeaderType.Animated) {
323
+ horizontalSafeAreaPadding()
324
+ }
325
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
326
+ verticalScroll(scrollState)
327
+ }
328
+ .conditional(useDuoGlass) {
329
+ padding(top = screenLayout.headerHeight)
330
+ }
331
+ .padding(
332
+ bottom = when {
333
+ options.hasFooter() || isBottomTabRoot -> 0.dp
334
+ isBottomTabChild -> bottomTabChildInset()
335
+ !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
336
+ else -> navigationBar
337
+ }
338
+ )
339
+ ) {
340
+ ScreenContent(content = content, insetContent = insetContent)
341
+ }
342
+
343
+ }
344
+
345
+ @Composable
346
+ internal fun ScreenContent(content: @Composable () -> Unit, insetContent: Boolean) {
347
+ val scrollState = LocalScrollState.current
348
+ val options = LocalOptions.current
349
+ val screenLayout = getScreenLayout()
350
+ val insetBody: @Composable () -> Unit = {
351
+ CompositionLocalProvider(
352
+ LocalScreenLayout provides if (insetContent) screenLayout.consumeHorizontalInsets() else screenLayout,
353
+ content = content,
354
+ )
355
+ }
356
+
357
+ if (options.headerType is HeaderType.Animated) {
358
+ val animatedHeader = options.headerType
359
+ Box {
360
+ val bannerModifier = animatedHeader.height
361
+ ?.let { Modifier.fillMaxWidth().height(it) }
362
+ ?: Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)
363
+ Box(bannerModifier) {
364
+ animatedHeader.composable.invoke(scrollState.value)
365
+ }
366
+ Box(
367
+ Modifier
368
+ .padding(top = screenLayout.headerHeight + animatedHeader.layoutOffSet)
369
+ .conditional(insetContent) { horizontalSafeAreaPadding() }
370
+ ) {
371
+ insetBody()
372
+ }
373
+ }
374
+ } else {
375
+ insetBody()
376
+ }
377
+ }
378
+
379
+ @Composable
380
+ internal fun FooterContent() {
381
+ val options = LocalOptions.current
382
+ if (options.hasFooter()) {
383
+ val keyboardSize = keyboardSizeState()
384
+ // the Duo floating tab bar overlays tab content, so a tab's footer sits above it
385
+ val duoTabInset = if (LocalNavigation.current.bottomTabIndex != -1) duoFloatingTabBarInset(getScreenLayout()) else 0.dp
386
+ val bottomPadding = (maxOf(AppNavigationBar.current, duoTabInset) - keyboardSize.value).coerceAtLeast(0.dp)
387
+ Footer(
388
+ footerComponent = options.footerComponent,
389
+ bottomPadding = bottomPadding,
390
+ transparent = options.footerTransparent,
391
+ useMargin = options.footerUseMargin
392
+ )
393
+ }
394
+ }
395
+
396
+ @Composable
397
+ internal fun keyboardSizeState(): State<Dp> {
398
+ val bottom = WindowInsets.ime.asPaddingValues()
399
+ return rememberUpdatedState(bottom.calculateBottomPadding())
400
+ }
401
+
402
+ @Composable
403
+ internal fun OverplayView(isBottomTabChild: Boolean, id: Int) {
404
+ val overplayType = OverplayComponentRegistry.getOverplayType()
405
+
406
+ if (overplayType != null) {
407
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 7f).fillMaxSize()) {
408
+ if (isBottomTabChild) return@Box
409
+ if (OverplayComponentRegistry.currentRootId() != id) return@Box
410
+ OverplayComponentRegistry.OverlayComponent()
411
+ }
412
+ }
413
+ }
414
+
415
+ @Composable
416
+ fun Footer(
417
+ footerComponent: @Composable (() -> Unit)?,
418
+ bottomPadding: Dp,
419
+ transparent: Boolean = false,
420
+ useMargin: Boolean = true,
421
+ ) {
422
+ if (footerComponent == null) return
423
+
424
+ val footerHeightPx = LocalFooterHeightPx.current
425
+
426
+ val shadowBrush = remember {
427
+ Brush.verticalGradient(
428
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
429
+ )
430
+ }
431
+
432
+ val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
433
+
434
+ Box(Modifier.onGloballyPositioned {
435
+ if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
436
+ }) {
437
+ Box(
438
+ Modifier
439
+ .fillMaxWidth()
440
+ .background(backgroundColor)
441
+ .conditional(IsShowBaseLineDebug) {
442
+ border(1.dp, Colors.blue_03)
443
+ }
444
+ .horizontalSafeAreaPadding()
445
+ .padding(
446
+ top = if (useMargin) Spacing.S else 0.dp,
447
+ start = if (useMargin) Spacing.M else 0.dp,
448
+ end = if (useMargin) Spacing.M else 0.dp,
449
+ bottom = bottomPadding + if (useMargin) Spacing.S else 0.dp,
450
+ )
451
+ ) {
452
+ footerComponent.invoke()
453
+ }
454
+
455
+ if (!transparent) {
456
+ Box(
457
+ modifier = Modifier
458
+ .fillMaxWidth()
459
+ .height(6.dp)
460
+ .offset(x = 0.dp, y = (-6).dp)
461
+ .background(shadowBrush)
462
+ )
463
+ }
464
+ }
465
+ }
466
+
467
+ data class InputSearchLayoutParams(
468
+ val topPadding: Dp,
469
+ val startPadding: Dp,
470
+ val endPadding: Dp
471
+ )
472
+
473
+ @Suppress("FrequentlyChangingValue")
474
+ @Composable
475
+ internal fun SearchAnimated(
476
+ snapState: ScrollableState
477
+ ) {
478
+ val scrollState = LocalScrollState.current
479
+ val options = LocalOptions.current
480
+ val navigator = LocalNavigator.current
481
+ val density = LocalDensity.current
482
+
483
+ val scrollDp = with(density) { scrollState.value.toDp() }
484
+
485
+ val inputSearchType = getInputSearchType(options)
486
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
487
+ val headerRightWidthDp = with(density) { headerRightWidthPx.intValue.toDp() }
488
+
489
+ if (inputSearchType == InputSearchType.None) return
490
+ val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
491
+
492
+ val screenLayout = getScreenLayout()
493
+ val useSideBar = screenLayout.sideBar != null
494
+ // collapsed, the search is centered in the bar row
495
+ val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
496
+ val maxTopPadding = screenLayout.headerHeight
497
+ val minStartPadding = Spacing.M
498
+ val maxStartPadding = if (options.hiddenBack || useSideBar) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
499
+ val minEndPadding = Spacing.M
500
+ val maxEndPadding = when {
501
+ options.headerRight is HeaderRight.None -> Spacing.M
502
+ useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
503
+ else -> headerRightWidthDp + Spacing.M * 2
504
+ }
505
+
506
+ val targetColor = lerp(
507
+ AppTheme.current.colors.background.surface,
508
+ AppTheme.current.colors.background.default,
509
+ (scrollDp / minTopPadding.coerceAtLeast(1.dp)).coerceIn(0f, 1f)
510
+ )
511
+ val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
512
+
513
+ val layoutParams = when (inputSearchType) {
514
+ InputSearchType.Header -> {
515
+ InputSearchLayoutParams(
516
+ topPadding = minTopPadding,
517
+ startPadding = maxStartPadding,
518
+ endPadding = maxEndPadding
519
+ )
520
+ }
521
+
522
+ InputSearchType.Animated -> {
523
+ val collapsePx = with(density) { HEADER_HEIGHT.dp.roundToPx() }
524
+ val progress = (scrollState.value / collapsePx.toFloat()).coerceIn(0f, 1f)
525
+
526
+ LaunchedEffect(snapState, collapsePx) {
527
+ snapshotFlow { snapState.isScrollInProgress }
528
+ .filter { !it }
529
+ .collect {
530
+ val offset = snapState.headerOffsetPx() ?: return@collect
531
+ if (offset <= 0 || offset >= collapsePx) return@collect
532
+ val target = if (offset < collapsePx / 2) 0 else collapsePx
533
+ launch { snapState.animateScrollBy((target - offset).toFloat()) }
534
+ }
535
+ }
536
+
537
+ InputSearchLayoutParams(
538
+ topPadding = lerp(maxTopPadding, minTopPadding, progress),
539
+ startPadding = lerp(minStartPadding, maxStartPadding, progress),
540
+ endPadding = lerp(minEndPadding, maxEndPadding, progress)
541
+ )
542
+ }
543
+
544
+ InputSearchType.None -> return
545
+ }
546
+
547
+ Spacer(Modifier.height(layoutParams.topPadding))
548
+ Box(
549
+ modifier = Modifier
550
+ .height(HEADER_HEIGHT.dp)
551
+ .fillMaxWidth()
552
+ .padding(
553
+ start = layoutParams.startPadding,
554
+ end = layoutParams.endPadding
555
+ ),
556
+ contentAlignment = Alignment.Center
557
+ ) {
558
+ InputSearch(
559
+ inputSearchProps = inputSearchProps.copy(
560
+ backgroundColor = animatedColor,
561
+ onPressButtonText = {
562
+ navigator.pop()
563
+ }
564
+ )
565
+ )
566
+ }
567
+ }
568
+
569
+ private fun ScrollableState.headerOffsetPx(): Int? = when (this) {
570
+ is LazyListState -> if (firstVisibleItemIndex == 0) firstVisibleItemScrollOffset else null
571
+ is ScrollState -> value
572
+ else -> null
573
+ }
574
+
575
+ private fun quantize(value: Int, stepPx: Int): Int {
576
+ if (stepPx <= 1) return value
577
+ return (value / stepPx) * stepPx
578
+ }
579
+
580
+ @Composable
581
+ internal fun LazyListState.proxyScrollState(
582
+ thresholdPx: Int = 200,
583
+ stepPx: Int = 4
584
+ ): Pair<ScrollState, Boolean> = proxyScroll(
585
+ source = this,
586
+ thresholdPx = thresholdPx,
587
+ stepPx = stepPx,
588
+ isScrollInProgress = { isScrollInProgress },
589
+ rawTargetFlow = {
590
+ snapshotFlow {
591
+ if (firstVisibleItemIndex == 0) firstVisibleItemScrollOffset
592
+ else SCROLLED_PAST_FIRST_ITEM_THRESHOLD
593
+ }
594
+ },
595
+ )
596
+
597
+ @Composable
598
+ internal fun ScrollState.proxyLimitedScrollState(
599
+ thresholdPx: Int = 200,
600
+ stepPx: Int = 4
601
+ ): Pair<ScrollState, Boolean> = proxyScroll(
602
+ source = this,
603
+ thresholdPx = thresholdPx,
604
+ stepPx = stepPx,
605
+ isScrollInProgress = { isScrollInProgress },
606
+ rawTargetFlow = { snapshotFlow { value } },
607
+ )
608
+
609
+ @Composable
610
+ private fun proxyScroll(
611
+ source: Any,
612
+ thresholdPx: Int,
613
+ stepPx: Int,
614
+ isScrollInProgress: () -> Boolean,
615
+ rawTargetFlow: () -> Flow<Int>,
616
+ ): Pair<ScrollState, Boolean> {
617
+ val proxy = rememberScrollState()
618
+ val locked = remember { mutableStateOf(false) }
619
+
620
+ LaunchedEffect(source, proxy, thresholdPx, stepPx) {
621
+ rawTargetFlow().collect { rawTarget ->
622
+ val shouldLock = rawTarget > thresholdPx
623
+ if (locked.value != shouldLock) locked.value = shouldLock
624
+
625
+ val desired = if (shouldLock) thresholdPx else quantize(rawTarget, stepPx)
626
+
627
+ if (proxy.value != desired) {
628
+ proxy.scrollTo(desired.coerceAtLeast(0))
629
+ }
630
+ }
631
+ }
632
+
633
+ val inProgress = remember { mutableStateOf(false) }
634
+ LaunchedEffect(source, thresholdPx) {
635
+ snapshotFlow { isScrollInProgress() }
636
+ .collect { progressing ->
637
+ inProgress.value = if (locked.value) false else progressing
638
+ }
639
+ }
640
+
641
+ return proxy to inProgress.value
642
+ }
643
+
644
+ private const val SCROLLED_PAST_FIRST_ITEM_THRESHOLD = 10_000
645
+
646
+ internal val LocalScrollState = staticCompositionLocalOf<ScrollState> { error("No Scroll State provided") }
647
+ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error("No NavigationOptions provided") }
648
+
649
+ val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
650
+
651
+ // iPhone Duo side bar: the header leaves with the content and slides back in on a reverse scroll
652
+ // (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
653
+ @Composable
654
+ private fun rememberQuickReturnHide(
655
+ enabled: Boolean,
656
+ scrollState: ScrollState,
657
+ heightPx: Float,
658
+ ): Animatable<Float, AnimationVector1D> {
659
+ val hide = remember { Animatable(0f) }
660
+ LaunchedEffect(enabled, scrollState, heightPx) {
661
+ if (!enabled) {
662
+ hide.snapTo(0f)
663
+ return@LaunchedEffect
664
+ }
665
+ var last = scrollState.value
666
+ snapshotFlow { scrollState.value }.collect { y ->
667
+ val delta = y - last
668
+ last = y
669
+ hide.snapTo(if (y <= 0) 0f else (hide.value + delta).coerceIn(0f, heightPx))
670
+ }
671
+ }
672
+ // a half-shown header settles fully in or out once the scroll stops
673
+ LaunchedEffect(enabled, scrollState, heightPx) {
674
+ if (!enabled) return@LaunchedEffect
675
+ snapshotFlow { scrollState.isScrollInProgress }
676
+ .filter { !it }
677
+ .collect {
678
+ val value = hide.value
679
+ // within the first header height the header still sits on the content
680
+ if (scrollState.value < heightPx || value <= 0f || value >= heightPx) return@collect
681
+ launch { hide.animateTo(if (value > heightPx / 2) heightPx else 0f, tween(200)) }
682
+ }
683
+ }
684
+ return hide
685
+ }
686
+
687
+ internal fun NavigationOptions.usesDuoQuickReturnHeader(layout: ScreenLayout): Boolean =
688
+ usesDuoGlassHeader(layout) || (layout.sideBar != null && headerType is HeaderType.Animated &&
689
+ scrollData.scrollable && scrollData.scrollState is ScrollState)
690
+
691
+ // iPhone Duo side bar: content scrolls under a glass header instead of starting below it (RN Screen).
692
+ internal fun NavigationOptions.usesDuoGlassHeader(layout: ScreenLayout): Boolean =
693
+ layout.sideBar != null && headerType is HeaderType.DefaultOrExtended &&
694
+ getInputSearchType(this) == InputSearchType.None &&
695
+ scrollData.scrollable && scrollData.scrollState is ScrollState
696
+
697
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
698
+ return when (val headerType = options.headerType) {
699
+ is HeaderType.DefaultOrExtended -> when {
700
+ headerType.inputSearchProps == null -> InputSearchType.None
701
+ headerType.useAnimated -> InputSearchType.Animated
702
+ else -> InputSearchType.Header
703
+ }
704
+
705
+ else -> InputSearchType.None
706
+ }
707
+ }
708
+