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