@momo-kits/native-kits 0.162.2-test.25 → 0.162.2-test.26-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 (137) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/build.gradle.kts +190 -0
  3. package/compose/build.gradle.kts.backup +190 -0
  4. package/compose/compose.podspec +47 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +150 -0
  7. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  8. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  9. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  10. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  11. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +101 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +294 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +107 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +140 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +17 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +270 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +73 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +48 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +51 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +252 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +100 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +154 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +360 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +597 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +128 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +69 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +207 -0
  129. package/gradle/libs.versions.toml +69 -0
  130. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  131. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  132. package/gradle.properties +26 -0
  133. package/gradlew +252 -0
  134. package/gradlew.bat +94 -0
  135. package/local.properties +8 -0
  136. package/package.json +1 -1
  137. package/settings.gradle.kts +64 -0
@@ -0,0 +1,597 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.foundation.ScrollState
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.border
8
+ import androidx.compose.foundation.gestures.ScrollableState
9
+ import androidx.compose.foundation.layout.Arrangement
10
+ import androidx.compose.foundation.layout.Box
11
+ import androidx.compose.foundation.layout.Column
12
+ import androidx.compose.foundation.layout.ColumnScope
13
+ import androidx.compose.foundation.layout.Spacer
14
+ import androidx.compose.foundation.layout.WindowInsets
15
+ import androidx.compose.foundation.layout.asPaddingValues
16
+ import androidx.compose.foundation.layout.aspectRatio
17
+ import androidx.compose.foundation.layout.fillMaxSize
18
+ import androidx.compose.foundation.layout.fillMaxWidth
19
+ import androidx.compose.foundation.layout.height
20
+ import androidx.compose.foundation.layout.ime
21
+ import androidx.compose.foundation.layout.imePadding
22
+ import androidx.compose.foundation.layout.offset
23
+ import androidx.compose.foundation.layout.padding
24
+ import androidx.compose.foundation.lazy.LazyListState
25
+ import androidx.compose.foundation.lazy.rememberLazyListState
26
+ import androidx.compose.foundation.rememberScrollState
27
+ import androidx.compose.foundation.verticalScroll
28
+ import androidx.compose.material.ExperimentalMaterialApi
29
+ import androidx.compose.runtime.Composable
30
+ import androidx.compose.runtime.CompositionLocalProvider
31
+ import androidx.compose.runtime.LaunchedEffect
32
+ import androidx.compose.runtime.State
33
+ import androidx.compose.runtime.getValue
34
+ import androidx.compose.runtime.mutableIntStateOf
35
+ import androidx.compose.runtime.mutableStateOf
36
+ import androidx.compose.runtime.remember
37
+ import androidx.compose.runtime.rememberUpdatedState
38
+ import androidx.compose.runtime.snapshotFlow
39
+ import androidx.compose.runtime.staticCompositionLocalOf
40
+ import androidx.compose.ui.Alignment
41
+ import androidx.compose.ui.Modifier
42
+ import androidx.compose.ui.graphics.Brush
43
+ import androidx.compose.ui.graphics.Color
44
+ import androidx.compose.ui.graphics.lerp
45
+ import androidx.compose.ui.layout.onGloballyPositioned
46
+ import androidx.compose.ui.platform.LocalDensity
47
+ import androidx.compose.ui.unit.Dp
48
+ import androidx.compose.ui.unit.dp
49
+ import androidx.compose.ui.zIndex
50
+ import kotlinx.coroutines.flow.Flow
51
+ import vn.momo.kits.components.InputSearch
52
+ import vn.momo.kits.const.AppNavigationBar
53
+ import vn.momo.kits.const.AppStatusBar
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.HeaderType
68
+ import vn.momo.kits.navigation.component.InputSearchType
69
+ import vn.momo.kits.platform.BackHandler
70
+ import vn.momo.kits.platform.supportsImePadding
71
+ import vn.momo.kits.navigation.tracking.ScreenTracker
72
+ import vn.momo.kits.navigation.tracking.ScreenTrackingState
73
+ import kotlinx.coroutines.delay
74
+ import vn.momo.kits.application.LocalContext
75
+ import kotlin.time.Clock
76
+ import kotlin.time.Duration.Companion.milliseconds
77
+ import kotlin.time.ExperimentalTime
78
+
79
+ internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
80
+ internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
81
+
82
+ @Composable
83
+ private fun bottomTabBarInset(): Dp {
84
+ if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
85
+ val keyboard = keyboardSizeState().value
86
+ return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
87
+ }
88
+
89
+ @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
90
+ @Composable
91
+ internal fun StackScreen(
92
+ content: @Composable () -> Unit,
93
+ navigationOptions: NavigationOptions? = null,
94
+ id: Int = -1,
95
+ name: String = "",
96
+ bottomTabIndex: Int = -1,
97
+ onBackHandler: (() -> Unit)? = null,
98
+ ) {
99
+ val navigator = LocalNavigator.current
100
+ val maxApi = LocalMaxApi.current
101
+ val context = LocalContext.current
102
+ val statusBar = AppStatusBar.current
103
+ val density = LocalDensity.current
104
+ val screenRegistry = LocalDynamicScreenRegistry.current
105
+ val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
106
+
107
+ val options by navigation.currentOptions
108
+ val isBottomTabChild = bottomTabIndex != -1
109
+ val isBottomTabRoot = navigation.isBottomTabRoot
110
+
111
+ // Auto tracking state
112
+ val trackingState = remember {
113
+ ScreenTrackingState().apply {
114
+ time = Clock.System.now().toEpochMilliseconds()
115
+ }
116
+ }
117
+
118
+ // Determine action: push or back
119
+ val action = remember {
120
+ if (ScreenTracker.getLastScreenName() != null) "push" else "back"
121
+ }
122
+
123
+ // Auto tracking effects
124
+ LaunchedEffect(name) {
125
+ // Track screen navigated immediately
126
+ ScreenTracker.trackScreenNavigated(
127
+ maxApi = maxApi,
128
+ context = context,
129
+ screenName = name,
130
+ action = action,
131
+ state = trackingState
132
+ )
133
+
134
+ // Track screen displayed after 2 seconds (debounce)
135
+ delay(2000.milliseconds)
136
+ val loadTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
137
+ ScreenTracker.trackScreenDisplayed(
138
+ maxApi = maxApi,
139
+ context = context,
140
+ screenName = name,
141
+ duration = loadTime,
142
+ state = trackingState
143
+ )
144
+
145
+ // Track screen interacted after displayed
146
+ val interactionTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
147
+ ScreenTracker.trackScreenInteracted(
148
+ maxApi = maxApi,
149
+ context = context,
150
+ screenName = name,
151
+ duration = interactionTime - loadTime,
152
+ totalDuration = interactionTime,
153
+ state = trackingState
154
+ )
155
+ }
156
+
157
+ val limit = with(density) {
158
+ (statusBar).toPx() + HEADER_HEIGHT
159
+ }.toInt()
160
+
161
+ val (scrollState, scrollInProcess) = if (options.scrollData.scrollState is LazyListState)
162
+ (options.scrollData.scrollState as? LazyListState ?: rememberLazyListState()).proxyScrollState(limit, 15)
163
+ else
164
+ (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 15)
165
+
166
+ val footerHeightPx = remember { mutableIntStateOf(0) }
167
+ val headerRightWidthPx = remember { mutableIntStateOf(0) }
168
+
169
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
170
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
171
+ }
172
+
173
+ BackHandler(true) { navigator.onBackSafe() }
174
+
175
+ CompositionLocalProvider(
176
+ StackScreenScrollableState provides options.scrollData.scrollState,
177
+ LocalNavigation provides navigation,
178
+ LocalScrollState provides scrollState,
179
+ LocalOptions provides options,
180
+ LocalFooterHeightPx provides footerHeightPx,
181
+ LocalHeaderRightWidthPx provides headerRightWidthPx
182
+ ) {
183
+ Box(
184
+ Modifier
185
+ .fillMaxSize()
186
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
187
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
188
+ hideKeyboardOnTap()
189
+ }
190
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
191
+ imePadding()
192
+ }
193
+ ) {
194
+ Box(Modifier.zIndex(1f)) {
195
+ HeaderBackground()
196
+ }
197
+
198
+ Box(Modifier.zIndex(4f)) {
199
+ Header(onBackHandler)
200
+ }
201
+
202
+ Column(Modifier.zIndex(5f)) {
203
+ SearchAnimated(isScrollInProgress = scrollInProcess)
204
+ }
205
+
206
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
207
+ MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
208
+ FooterContent()
209
+ }
210
+
211
+ Box(Modifier.zIndex(6f)) {
212
+ FloatingContent(isBottomTabChild = isBottomTabChild)
213
+ }
214
+
215
+ OverplayView(isBottomTabChild = isBottomTabChild, id = id)
216
+ }
217
+ }
218
+ }
219
+
220
+ @Suppress("FrequentlyChangingValue")
221
+ @Composable
222
+ internal fun FloatingContent(isBottomTabChild: Boolean) {
223
+ val options = LocalOptions.current
224
+ val density = LocalDensity.current
225
+ val footerHeightPx = LocalFooterHeightPx.current
226
+ val scrollState = LocalScrollState.current
227
+ val navigationBar = AppNavigationBar.current
228
+
229
+ val fabProps = options.floatingButtonProps ?: return
230
+ val bottomPadding = fabProps.bottom
231
+ ?: (Spacing.M + when {
232
+ options.hasFooter() -> with(density) { footerHeightPx.intValue.toDp() }
233
+ isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
234
+ else -> navigationBar
235
+ })
236
+
237
+ FloatingButton(
238
+ scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
239
+ onClick = fabProps.onClick,
240
+ containerColor = AppTheme.current.colors.primary,
241
+ bottom = bottomPadding,
242
+ icon = fabProps.icon,
243
+ iconColor = fabProps.iconColor,
244
+ text = fabProps.label,
245
+ size = fabProps.size,
246
+ position = fabProps.position ?: FABPosition.END,
247
+ )
248
+ }
249
+
250
+ @Composable
251
+ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTabRoot: Boolean, isBottomTabChild: Boolean) {
252
+ val options = LocalOptions.current
253
+ val inputSearchType = getInputSearchType(options)
254
+ val density = LocalDensity.current
255
+ val scrollState = LocalScrollState.current
256
+ val navigationBar = AppNavigationBar.current
257
+
258
+ Spacer(
259
+ Modifier.height(
260
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
261
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
262
+ )
263
+ )
264
+ if (inputSearchType == InputSearchType.Animated) {
265
+ val scrollDp = with(density) { scrollState.value.toDp() }
266
+
267
+ val animatedTopPadding by animateDpAsState(
268
+ targetValue = (HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp),
269
+ label = "AnimatedTopPadding"
270
+ )
271
+ Spacer(Modifier.height(animatedTopPadding))
272
+ }
273
+ Column(
274
+ Modifier
275
+ .fillMaxWidth()
276
+ .weight(1f)
277
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
278
+ verticalScroll(scrollState)
279
+ }
280
+ .padding(
281
+ bottom = when {
282
+ options.hasFooter() || isBottomTabRoot -> 0.dp
283
+ isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
284
+ !options.scrollData.scrollable -> 0.dp
285
+ else -> navigationBar
286
+ }
287
+ ),
288
+ // The React kit's Screen puts the gap BETWEEN a screen's blocks, not the
289
+ // blocks themselves: with `useGridLayout` (default on) it wraps every
290
+ // direct child in `<View style={{marginTop: Spacing.M}}>`
291
+ // (foundation/Layout/Screen.tsx renderContent). Compose cannot walk the
292
+ // content lambda's children to do that per-child, so space them here —
293
+ // this is the closest faithful equivalent and covers the normal screen.
294
+ //
295
+ // Two React behaviours it deliberately does NOT reproduce, because
296
+ // Compose gives no way to: the 12dp React also puts ABOVE the first
297
+ // child, and the per-child `useMargin = false` opt-out (React reads the
298
+ // child's props; a @Composable lambda exposes nothing). Generated code
299
+ // therefore still emits its own per-block wrapper, which does honour
300
+ // `useMargin: false` — and does not double up here, since a generated
301
+ // screen enters as a single child.
302
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
303
+ ) {
304
+ ScreenContent(content = content)
305
+ }
306
+
307
+ }
308
+
309
+ @Composable
310
+ internal fun ScreenContent(content: @Composable () -> Unit) {
311
+ val scrollState = LocalScrollState.current
312
+ val options = LocalOptions.current
313
+
314
+ if (options.headerType is HeaderType.Animated) {
315
+ val animatedHeader = options.headerType
316
+ Box {
317
+ Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
318
+ animatedHeader.composable.invoke(scrollState.value)
319
+ }
320
+ Box(Modifier.padding(top = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
321
+ content()
322
+ }
323
+ }
324
+ } else {
325
+ content()
326
+ }
327
+ }
328
+
329
+ @Composable
330
+ internal fun FooterContent() {
331
+ val options = LocalOptions.current
332
+ if (options.hasFooter()) {
333
+ val keyboardSize = keyboardSizeState()
334
+ val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
335
+ Footer(footerComponent = options.footerComponent, bottomPadding = bottomPadding)
336
+ }
337
+ }
338
+
339
+ @Composable
340
+ internal fun keyboardSizeState(): State<Dp> {
341
+ val bottom = WindowInsets.ime.asPaddingValues()
342
+ return rememberUpdatedState(bottom.calculateBottomPadding())
343
+ }
344
+
345
+ @Composable
346
+ internal fun OverplayView(isBottomTabChild: Boolean, id: Int) {
347
+ val overplayType = OverplayComponentRegistry.getOverplayType()
348
+
349
+ if (overplayType != null) {
350
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 7f).fillMaxSize()) {
351
+ if (isBottomTabChild) return@Box
352
+ if (OverplayComponentRegistry.currentRootId() != id) return@Box
353
+ OverplayComponentRegistry.OverlayComponent()
354
+ }
355
+ }
356
+ }
357
+
358
+ @Composable
359
+ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
360
+ if (footerComponent == null) return
361
+
362
+ val footerHeightPx = LocalFooterHeightPx.current
363
+
364
+ val shadowBrush = remember {
365
+ Brush.verticalGradient(
366
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
367
+ )
368
+ }
369
+
370
+ Box(Modifier.onGloballyPositioned {
371
+ if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
372
+ }) {
373
+ Box(
374
+ Modifier
375
+ .fillMaxWidth()
376
+ .background(AppTheme.current.colors.background.surface)
377
+ .conditional(IsShowBaseLineDebug) {
378
+ border(1.dp, Colors.blue_03)
379
+ }
380
+ .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
381
+ ) {
382
+ footerComponent.invoke()
383
+ }
384
+
385
+ Box(
386
+ modifier = Modifier
387
+ .fillMaxWidth()
388
+ .height(6.dp)
389
+ .offset(x = 0.dp, y = (-6).dp)
390
+ .background(shadowBrush)
391
+ )
392
+ }
393
+ }
394
+
395
+ data class InputSearchLayoutParams(
396
+ val topPadding: Dp,
397
+ val startPadding: Dp,
398
+ val endPadding: Dp
399
+ )
400
+
401
+ @Suppress("FrequentlyChangingValue")
402
+ @Composable
403
+ internal fun SearchAnimated(
404
+ isScrollInProgress: Boolean
405
+ ) {
406
+ val scrollState = LocalScrollState.current
407
+ val options = LocalOptions.current
408
+ val navigator = LocalNavigator.current
409
+ val density = LocalDensity.current
410
+
411
+ val scrollDp = with(density) { scrollState.value.toDp() }
412
+
413
+ val inputSearchType = getInputSearchType(options)
414
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
415
+ val headerRightWidthDp = with(density) { headerRightWidthPx.intValue.toDp() }
416
+
417
+ if (inputSearchType == InputSearchType.None) return
418
+ val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
419
+
420
+ val minTopPadding = AppStatusBar.current
421
+ val maxTopPadding = AppStatusBar.current + HEADER_HEIGHT.dp
422
+ val minStartPadding = Spacing.M
423
+ val maxStartPadding = if (options.hiddenBack) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
424
+ val minEndPadding = Spacing.M
425
+ val maxEndPadding = headerRightWidthDp + if (options.headerRight is HeaderRight.None) Spacing.M else Spacing.M * 2
426
+
427
+ val targetColor = lerp(
428
+ AppTheme.current.colors.background.surface,
429
+ AppTheme.current.colors.background.default,
430
+ (scrollDp / minTopPadding).coerceIn(0f, 1f)
431
+ )
432
+ val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
433
+
434
+ val layoutParams = when (inputSearchType) {
435
+ InputSearchType.Header -> {
436
+ InputSearchLayoutParams(
437
+ topPadding = minTopPadding,
438
+ startPadding = maxStartPadding,
439
+ endPadding = maxEndPadding
440
+ )
441
+ }
442
+
443
+ InputSearchType.Animated -> {
444
+ val animatedTopPadding by animateDpAsState(
445
+ targetValue = (maxTopPadding - scrollDp).coerceIn(minTopPadding, maxTopPadding),
446
+ label = "AnimatedTopPadding"
447
+ )
448
+
449
+ val animatedStartPadding by animateDpAsState(
450
+ targetValue = (minStartPadding + scrollDp).coerceIn(minStartPadding, maxStartPadding),
451
+ label = "AnimatedStartPadding"
452
+ )
453
+
454
+ val animatedEndPadding by animateDpAsState(
455
+ targetValue = (minEndPadding + scrollDp).coerceIn(minEndPadding, maxEndPadding),
456
+ label = "AnimatedEndPadding"
457
+ )
458
+
459
+ val maxPadding = remember(maxTopPadding, maxStartPadding, maxEndPadding) {
460
+ maxOf(maxEndPadding, maxStartPadding, maxTopPadding)
461
+ }
462
+ val snapScrollValue = with(density) { maxPadding.toPx().toInt() }
463
+
464
+ LaunchedEffect(isScrollInProgress && scrollState.value != 0 && scrollState.value != snapScrollValue) {
465
+ if (scrollDp < maxPadding) {
466
+ if (!isScrollInProgress) {
467
+ val midpoint = (maxTopPadding - minTopPadding) / 2
468
+
469
+ if (scrollDp < midpoint) {
470
+ scrollState.animateScrollTo(0)
471
+ } else {
472
+ scrollState.animateScrollTo(snapScrollValue)
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ InputSearchLayoutParams(
479
+ topPadding = animatedTopPadding,
480
+ startPadding = animatedStartPadding,
481
+ endPadding = animatedEndPadding
482
+ )
483
+ }
484
+
485
+ InputSearchType.None -> return
486
+ }
487
+
488
+ Spacer(Modifier.height(layoutParams.topPadding))
489
+ Box(
490
+ modifier = Modifier
491
+ .height(HEADER_HEIGHT.dp)
492
+ .fillMaxWidth()
493
+ .padding(
494
+ start = layoutParams.startPadding,
495
+ end = layoutParams.endPadding
496
+ ),
497
+ contentAlignment = Alignment.Center
498
+ ) {
499
+ InputSearch(
500
+ inputSearchProps = inputSearchProps.copy(
501
+ backgroundColor = animatedColor,
502
+ onPressButtonText = {
503
+ navigator.pop()
504
+ }
505
+ )
506
+ )
507
+ }
508
+ }
509
+
510
+ private fun quantize(value: Int, stepPx: Int): Int {
511
+ if (stepPx <= 1) return value
512
+ return (value / stepPx) * stepPx
513
+ }
514
+
515
+ @Composable
516
+ internal fun LazyListState.proxyScrollState(
517
+ thresholdPx: Int = 200,
518
+ stepPx: Int = 4
519
+ ): Pair<ScrollState, Boolean> = proxyScroll(
520
+ source = this,
521
+ thresholdPx = thresholdPx,
522
+ stepPx = stepPx,
523
+ isScrollInProgress = { isScrollInProgress },
524
+ rawTargetFlow = {
525
+ snapshotFlow {
526
+ if (firstVisibleItemIndex == 0) firstVisibleItemScrollOffset
527
+ else SCROLLED_PAST_FIRST_ITEM_THRESHOLD
528
+ }
529
+ },
530
+ )
531
+
532
+ @Composable
533
+ internal fun ScrollState.proxyLimitedScrollState(
534
+ thresholdPx: Int = 200,
535
+ stepPx: Int = 4
536
+ ): Pair<ScrollState, Boolean> = proxyScroll(
537
+ source = this,
538
+ thresholdPx = thresholdPx,
539
+ stepPx = stepPx,
540
+ isScrollInProgress = { isScrollInProgress },
541
+ rawTargetFlow = { snapshotFlow { value } },
542
+ )
543
+
544
+ @Composable
545
+ private fun proxyScroll(
546
+ source: Any,
547
+ thresholdPx: Int,
548
+ stepPx: Int,
549
+ isScrollInProgress: () -> Boolean,
550
+ rawTargetFlow: () -> Flow<Int>,
551
+ ): Pair<ScrollState, Boolean> {
552
+ val proxy = rememberScrollState()
553
+ val locked = remember { mutableStateOf(false) }
554
+
555
+ LaunchedEffect(source, proxy, thresholdPx, stepPx) {
556
+ rawTargetFlow().collect { rawTarget ->
557
+ val shouldLock = rawTarget > thresholdPx
558
+ if (locked.value != shouldLock) locked.value = shouldLock
559
+
560
+ val desired = if (shouldLock) thresholdPx else quantize(rawTarget, stepPx)
561
+
562
+ if (proxy.value != desired) {
563
+ proxy.scrollTo(desired.coerceAtLeast(0))
564
+ }
565
+ }
566
+ }
567
+
568
+ val inProgress = remember { mutableStateOf(false) }
569
+ LaunchedEffect(source, thresholdPx) {
570
+ snapshotFlow { isScrollInProgress() }
571
+ .collect { progressing ->
572
+ inProgress.value = if (locked.value) false else progressing
573
+ }
574
+ }
575
+
576
+ return proxy to inProgress.value
577
+ }
578
+
579
+ private const val SCROLLED_PAST_FIRST_ITEM_THRESHOLD = 10_000
580
+
581
+ internal val LocalScrollState = staticCompositionLocalOf<ScrollState> { error("No Scroll State provided") }
582
+ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error("No NavigationOptions provided") }
583
+
584
+ val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
585
+
586
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
587
+ return when (val headerType = options.headerType) {
588
+ is HeaderType.DefaultOrExtended -> when {
589
+ headerType.inputSearchProps == null -> InputSearchType.None
590
+ headerType.useAnimated -> InputSearchType.Animated
591
+ else -> InputSearchType.Header
592
+ }
593
+
594
+ else -> InputSearchType.None
595
+ }
596
+ }
597
+