@momo-kits/native-kits 0.167.1-beta.4 → 0.167.1-duo.1-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 (155) hide show
  1. package/.claude/settings.local.json +178 -7
  2. package/CLAUDE.md +7 -0
  3. package/build.gradle.kts +11 -0
  4. package/compose/build.gradle.kts +190 -0
  5. package/compose/build.gradle.kts.backup +190 -0
  6. package/compose/compose.podspec +47 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +151 -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 +334 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +98 -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 +52 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +38 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +39 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +201 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +596 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -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 +360 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +83 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -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 +37 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +364 -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 +127 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +71 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +115 -0
  129. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +213 -0
  135. package/gradle/libs.versions.toml +69 -0
  136. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  137. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  138. package/gradle.properties +26 -0
  139. package/gradlew +252 -0
  140. package/gradlew.bat +94 -0
  141. package/ios/Application/Navigation/Overplay/ModalScreen.swift +3 -0
  142. package/ios/Popup/PopupDisplay.swift +1 -0
  143. package/ios/Popup/PopupNotify.swift +8 -1
  144. package/ios/Popup/PopupPromotion.swift +3 -3
  145. package/ios/Typography/FontScale.swift +3 -3
  146. package/ios/native-kits.podspec +1 -1
  147. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  148. package/ios-demo/README.md +120 -0
  149. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  150. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  151. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  152. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  153. package/local.properties +8 -0
  154. package/package.json +1 -1
  155. package/settings.gradle.kts +64 -0
@@ -0,0 +1,596 @@
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.AppStatusBar
55
+ import vn.momo.kits.const.AppTheme
56
+ import vn.momo.kits.application.IsShowBaseLineDebug
57
+ import vn.momo.kits.const.Colors
58
+ import vn.momo.kits.const.Spacing
59
+ import vn.momo.kits.modifier.conditional
60
+ import vn.momo.kits.modifier.hideKeyboardOnTap
61
+ import vn.momo.kits.navigation.bottomtab.BOTTOM_TAB_BAR_HEIGHT
62
+ import vn.momo.kits.navigation.component.FABPosition
63
+ import vn.momo.kits.navigation.component.FloatingButton
64
+ import vn.momo.kits.navigation.component.HEADER_HEIGHT
65
+ import vn.momo.kits.navigation.component.Header
66
+ import vn.momo.kits.navigation.component.HeaderBackground
67
+ import vn.momo.kits.navigation.component.HeaderRight
68
+ import vn.momo.kits.navigation.component.HeaderType
69
+ import vn.momo.kits.navigation.component.InputSearchType
70
+ import vn.momo.kits.platform.BackHandler
71
+ import vn.momo.kits.platform.supportsImePadding
72
+ import vn.momo.kits.navigation.tracking.ScreenTracker
73
+ import vn.momo.kits.navigation.tracking.ScreenTrackingState
74
+ import kotlinx.coroutines.delay
75
+ import vn.momo.kits.application.LocalContext
76
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
77
+ import kotlin.time.Clock
78
+ import kotlin.time.Duration.Companion.milliseconds
79
+ import kotlin.time.ExperimentalTime
80
+
81
+ internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
82
+ internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
83
+
84
+ @Composable
85
+ private fun bottomTabBarInset(): Dp {
86
+ if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
87
+ val keyboard = keyboardSizeState().value
88
+ return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
89
+ }
90
+
91
+ @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
92
+ @Composable
93
+ internal fun StackScreen(
94
+ content: @Composable () -> Unit,
95
+ navigationOptions: NavigationOptions? = null,
96
+ id: Int = -1,
97
+ name: String = "",
98
+ bottomTabIndex: Int = -1,
99
+ onBackHandler: (() -> Unit)? = null,
100
+ ) {
101
+ val navigator = LocalNavigator.current
102
+ val maxApi = LocalMaxApi.current
103
+ val context = LocalContext.current
104
+ val statusBar = AppStatusBar.current
105
+ val density = LocalDensity.current
106
+ val screenRegistry = LocalDynamicScreenRegistry.current
107
+ val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
108
+
109
+ val options by navigation.currentOptions
110
+ val isBottomTabChild = bottomTabIndex != -1
111
+ val isBottomTabRoot = navigation.isBottomTabRoot
112
+
113
+ // Auto tracking state
114
+ val trackingState = remember {
115
+ ScreenTrackingState().apply {
116
+ time = Clock.System.now().toEpochMilliseconds()
117
+ }
118
+ }
119
+
120
+ // Determine action: push or back
121
+ val action = remember {
122
+ if (ScreenTracker.getLastScreenName() != null) "push" else "back"
123
+ }
124
+
125
+ // Auto tracking effects
126
+ LaunchedEffect(name) {
127
+ // Track screen navigated immediately
128
+ ScreenTracker.trackScreenNavigated(
129
+ maxApi = maxApi,
130
+ context = context,
131
+ screenName = name,
132
+ action = action,
133
+ state = trackingState
134
+ )
135
+
136
+ // Track screen displayed after 2 seconds (debounce)
137
+ delay(2000.milliseconds)
138
+ val loadTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
139
+ ScreenTracker.trackScreenDisplayed(
140
+ maxApi = maxApi,
141
+ context = context,
142
+ screenName = name,
143
+ duration = loadTime,
144
+ state = trackingState
145
+ )
146
+
147
+ // Track screen interacted after displayed
148
+ val interactionTime = Clock.System.now().toEpochMilliseconds() - trackingState.time
149
+ ScreenTracker.trackScreenInteracted(
150
+ maxApi = maxApi,
151
+ context = context,
152
+ screenName = name,
153
+ duration = interactionTime - loadTime,
154
+ totalDuration = interactionTime,
155
+ state = trackingState
156
+ )
157
+ }
158
+
159
+ val limit = with(density) {
160
+ (statusBar + HEADER_HEIGHT.dp).toPx()
161
+ }.toInt()
162
+
163
+ val lazyListState = options.scrollData.scrollState as? LazyListState
164
+ val (scrollState, _) = if (lazyListState != null)
165
+ lazyListState.proxyScrollState(limit, 1)
166
+ else
167
+ (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 1)
168
+ val snapState: ScrollableState = lazyListState ?: scrollState
169
+
170
+ val footerHeightPx = remember { mutableIntStateOf(0) }
171
+ val headerRightWidthPx = remember { mutableIntStateOf(0) }
172
+
173
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
174
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
175
+ }
176
+
177
+ val canPop = navigator.canPop()
178
+
179
+ val hasCustomBackHandling = !SwipeBackConfig.enabled ||
180
+ OverplayComponentRegistry.isOverplayShowing() ||
181
+ !canPop
182
+
183
+ BackHandler(hasCustomBackHandling) { navigator.onBackSafe() }
184
+
185
+ CompositionLocalProvider(
186
+ StackScreenScrollableState provides options.scrollData.scrollState,
187
+ LocalNavigation provides navigation,
188
+ LocalScrollState provides scrollState,
189
+ LocalOptions provides options,
190
+ LocalFooterHeightPx provides footerHeightPx,
191
+ LocalHeaderRightWidthPx provides headerRightWidthPx
192
+ ) {
193
+ Box(
194
+ Modifier
195
+ .fillMaxSize()
196
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
197
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
198
+ hideKeyboardOnTap()
199
+ }
200
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
201
+ imePadding()
202
+ }
203
+ ) {
204
+ Box(Modifier.zIndex(1f)) {
205
+ HeaderBackground()
206
+ }
207
+
208
+ Box(Modifier.zIndex(4f)) {
209
+ Header(onBackHandler)
210
+ }
211
+
212
+ Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
213
+ SearchAnimated(snapState = snapState)
214
+ }
215
+
216
+ Column(Modifier.zIndex(2f).fillMaxSize().horizontalSafeAreaPadding()) {
217
+ MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
218
+ FooterContent()
219
+ }
220
+
221
+ Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
222
+ FloatingContent(isBottomTabChild = isBottomTabChild)
223
+ }
224
+
225
+ OverplayView(isBottomTabChild = isBottomTabChild, id = id)
226
+ }
227
+ }
228
+ }
229
+
230
+ @Suppress("FrequentlyChangingValue")
231
+ @Composable
232
+ internal fun FloatingContent(isBottomTabChild: Boolean) {
233
+ val options = LocalOptions.current
234
+ val density = LocalDensity.current
235
+ val footerHeightPx = LocalFooterHeightPx.current
236
+ val scrollState = LocalScrollState.current
237
+ val navigationBar = AppNavigationBar.current
238
+
239
+ val fabProps = options.floatingButtonProps ?: return
240
+ val bottomPadding = fabProps.bottom
241
+ ?: (Spacing.M + when {
242
+ options.hasFooter() -> with(density) { footerHeightPx.intValue.toDp() }
243
+ isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
244
+ else -> navigationBar
245
+ })
246
+
247
+ FloatingButton(
248
+ scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
249
+ onClick = fabProps.onClick,
250
+ containerColor = AppTheme.current.colors.primary,
251
+ bottom = bottomPadding,
252
+ icon = fabProps.icon,
253
+ iconColor = fabProps.iconColor,
254
+ text = fabProps.label,
255
+ size = fabProps.size,
256
+ position = fabProps.position ?: FABPosition.END,
257
+ )
258
+ }
259
+
260
+ @Composable
261
+ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTabRoot: Boolean, isBottomTabChild: Boolean) {
262
+ val options = LocalOptions.current
263
+ val inputSearchType = getInputSearchType(options)
264
+ val density = LocalDensity.current
265
+ val scrollState = LocalScrollState.current
266
+ val navigationBar = AppNavigationBar.current
267
+
268
+ Spacer(
269
+ Modifier.height(
270
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
271
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
272
+ )
273
+ )
274
+ if (inputSearchType == InputSearchType.Animated) {
275
+ val scrollDp = with(density) { scrollState.value.toDp() }
276
+
277
+ Spacer(Modifier.height((HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp)))
278
+ }
279
+ Column(
280
+ Modifier
281
+ .fillMaxWidth()
282
+ .weight(1f)
283
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
284
+ verticalScroll(scrollState)
285
+ }
286
+ .padding(
287
+ bottom = when {
288
+ options.hasFooter() || isBottomTabRoot -> 0.dp
289
+ isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
290
+ !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
291
+ else -> navigationBar
292
+ }
293
+ )
294
+ ) {
295
+ ScreenContent(content = content)
296
+ }
297
+
298
+ }
299
+
300
+ @Composable
301
+ internal fun ScreenContent(content: @Composable () -> Unit) {
302
+ val scrollState = LocalScrollState.current
303
+ val options = LocalOptions.current
304
+
305
+ if (options.headerType is HeaderType.Animated) {
306
+ val animatedHeader = options.headerType
307
+ Box {
308
+ val bannerModifier = animatedHeader.height
309
+ ?.let { Modifier.fillMaxWidth().height(it) }
310
+ ?: Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)
311
+ Box(bannerModifier) {
312
+ animatedHeader.composable.invoke(scrollState.value)
313
+ }
314
+ Box(Modifier.padding(top = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
315
+ content()
316
+ }
317
+ }
318
+ } else {
319
+ content()
320
+ }
321
+ }
322
+
323
+ @Composable
324
+ internal fun FooterContent() {
325
+ val options = LocalOptions.current
326
+ if (options.hasFooter()) {
327
+ val keyboardSize = keyboardSizeState()
328
+ val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
329
+ Footer(
330
+ footerComponent = options.footerComponent,
331
+ bottomPadding = bottomPadding,
332
+ transparent = options.footerTransparent,
333
+ useMargin = options.footerUseMargin
334
+ )
335
+ }
336
+ }
337
+
338
+ @Composable
339
+ internal fun keyboardSizeState(): State<Dp> {
340
+ val bottom = WindowInsets.ime.asPaddingValues()
341
+ return rememberUpdatedState(bottom.calculateBottomPadding())
342
+ }
343
+
344
+ @Composable
345
+ internal fun OverplayView(isBottomTabChild: Boolean, id: Int) {
346
+ val overplayType = OverplayComponentRegistry.getOverplayType()
347
+
348
+ if (overplayType != null) {
349
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 7f).fillMaxSize()) {
350
+ if (isBottomTabChild) return@Box
351
+ if (OverplayComponentRegistry.currentRootId() != id) return@Box
352
+ OverplayComponentRegistry.OverlayComponent()
353
+ }
354
+ }
355
+ }
356
+
357
+ @Composable
358
+ fun Footer(
359
+ footerComponent: @Composable (() -> Unit)?,
360
+ bottomPadding: Dp,
361
+ transparent: Boolean = false,
362
+ useMargin: Boolean = true,
363
+ ) {
364
+ if (footerComponent == null) return
365
+
366
+ val footerHeightPx = LocalFooterHeightPx.current
367
+
368
+ val shadowBrush = remember {
369
+ Brush.verticalGradient(
370
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
371
+ )
372
+ }
373
+
374
+ val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
375
+
376
+ Box(Modifier.onGloballyPositioned {
377
+ if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
378
+ }) {
379
+ Box(
380
+ Modifier
381
+ .fillMaxWidth()
382
+ .background(backgroundColor)
383
+ .conditional(IsShowBaseLineDebug) {
384
+ border(1.dp, Colors.blue_03)
385
+ }
386
+ .padding(
387
+ top = if (useMargin) Spacing.S else 0.dp,
388
+ start = if (useMargin) Spacing.M else 0.dp,
389
+ end = if (useMargin) Spacing.M else 0.dp,
390
+ bottom = bottomPadding + if (useMargin) Spacing.S else 0.dp,
391
+ )
392
+ ) {
393
+ footerComponent.invoke()
394
+ }
395
+
396
+ if (!transparent) {
397
+ Box(
398
+ modifier = Modifier
399
+ .fillMaxWidth()
400
+ .height(6.dp)
401
+ .offset(x = 0.dp, y = (-6).dp)
402
+ .background(shadowBrush)
403
+ )
404
+ }
405
+ }
406
+ }
407
+
408
+ data class InputSearchLayoutParams(
409
+ val topPadding: Dp,
410
+ val startPadding: Dp,
411
+ val endPadding: Dp
412
+ )
413
+
414
+ @Suppress("FrequentlyChangingValue")
415
+ @Composable
416
+ internal fun SearchAnimated(
417
+ snapState: ScrollableState
418
+ ) {
419
+ val scrollState = LocalScrollState.current
420
+ val options = LocalOptions.current
421
+ val navigator = LocalNavigator.current
422
+ val density = LocalDensity.current
423
+
424
+ val scrollDp = with(density) { scrollState.value.toDp() }
425
+
426
+ val inputSearchType = getInputSearchType(options)
427
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
428
+ val headerRightWidthDp = with(density) { headerRightWidthPx.intValue.toDp() }
429
+
430
+ if (inputSearchType == InputSearchType.None) return
431
+ val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
432
+
433
+ val minTopPadding = AppStatusBar.current
434
+ val maxTopPadding = AppStatusBar.current + HEADER_HEIGHT.dp
435
+ val minStartPadding = Spacing.M
436
+ val maxStartPadding = if (options.hiddenBack) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
437
+ val minEndPadding = Spacing.M
438
+ val maxEndPadding = headerRightWidthDp + if (options.headerRight is HeaderRight.None) Spacing.M else Spacing.M * 2
439
+
440
+ val targetColor = lerp(
441
+ AppTheme.current.colors.background.surface,
442
+ AppTheme.current.colors.background.default,
443
+ (scrollDp / minTopPadding).coerceIn(0f, 1f)
444
+ )
445
+ val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
446
+
447
+ val layoutParams = when (inputSearchType) {
448
+ InputSearchType.Header -> {
449
+ InputSearchLayoutParams(
450
+ topPadding = minTopPadding,
451
+ startPadding = maxStartPadding,
452
+ endPadding = maxEndPadding
453
+ )
454
+ }
455
+
456
+ InputSearchType.Animated -> {
457
+ val collapsePx = with(density) { HEADER_HEIGHT.dp.roundToPx() }
458
+ val progress = (scrollState.value / collapsePx.toFloat()).coerceIn(0f, 1f)
459
+
460
+ LaunchedEffect(snapState, collapsePx) {
461
+ snapshotFlow { snapState.isScrollInProgress }
462
+ .filter { !it }
463
+ .collect {
464
+ val offset = snapState.headerOffsetPx() ?: return@collect
465
+ if (offset <= 0 || offset >= collapsePx) return@collect
466
+ val target = if (offset < collapsePx / 2) 0 else collapsePx
467
+ launch { snapState.animateScrollBy((target - offset).toFloat()) }
468
+ }
469
+ }
470
+
471
+ InputSearchLayoutParams(
472
+ topPadding = lerp(maxTopPadding, minTopPadding, progress),
473
+ startPadding = lerp(minStartPadding, maxStartPadding, progress),
474
+ endPadding = lerp(minEndPadding, maxEndPadding, progress)
475
+ )
476
+ }
477
+
478
+ InputSearchType.None -> return
479
+ }
480
+
481
+ Spacer(Modifier.height(layoutParams.topPadding))
482
+ Box(
483
+ modifier = Modifier
484
+ .height(HEADER_HEIGHT.dp)
485
+ .fillMaxWidth()
486
+ .padding(
487
+ start = layoutParams.startPadding,
488
+ end = layoutParams.endPadding
489
+ ),
490
+ contentAlignment = Alignment.Center
491
+ ) {
492
+ InputSearch(
493
+ inputSearchProps = inputSearchProps.copy(
494
+ backgroundColor = animatedColor,
495
+ onPressButtonText = {
496
+ navigator.pop()
497
+ }
498
+ )
499
+ )
500
+ }
501
+ }
502
+
503
+ private fun ScrollableState.headerOffsetPx(): Int? = when (this) {
504
+ is LazyListState -> if (firstVisibleItemIndex == 0) firstVisibleItemScrollOffset else null
505
+ is ScrollState -> value
506
+ else -> null
507
+ }
508
+
509
+ private fun quantize(value: Int, stepPx: Int): Int {
510
+ if (stepPx <= 1) return value
511
+ return (value / stepPx) * stepPx
512
+ }
513
+
514
+ @Composable
515
+ internal fun LazyListState.proxyScrollState(
516
+ thresholdPx: Int = 200,
517
+ stepPx: Int = 4
518
+ ): Pair<ScrollState, Boolean> = proxyScroll(
519
+ source = this,
520
+ thresholdPx = thresholdPx,
521
+ stepPx = stepPx,
522
+ isScrollInProgress = { isScrollInProgress },
523
+ rawTargetFlow = {
524
+ snapshotFlow {
525
+ if (firstVisibleItemIndex == 0) firstVisibleItemScrollOffset
526
+ else SCROLLED_PAST_FIRST_ITEM_THRESHOLD
527
+ }
528
+ },
529
+ )
530
+
531
+ @Composable
532
+ internal fun ScrollState.proxyLimitedScrollState(
533
+ thresholdPx: Int = 200,
534
+ stepPx: Int = 4
535
+ ): Pair<ScrollState, Boolean> = proxyScroll(
536
+ source = this,
537
+ thresholdPx = thresholdPx,
538
+ stepPx = stepPx,
539
+ isScrollInProgress = { isScrollInProgress },
540
+ rawTargetFlow = { snapshotFlow { value } },
541
+ )
542
+
543
+ @Composable
544
+ private fun proxyScroll(
545
+ source: Any,
546
+ thresholdPx: Int,
547
+ stepPx: Int,
548
+ isScrollInProgress: () -> Boolean,
549
+ rawTargetFlow: () -> Flow<Int>,
550
+ ): Pair<ScrollState, Boolean> {
551
+ val proxy = rememberScrollState()
552
+ val locked = remember { mutableStateOf(false) }
553
+
554
+ LaunchedEffect(source, proxy, thresholdPx, stepPx) {
555
+ rawTargetFlow().collect { rawTarget ->
556
+ val shouldLock = rawTarget > thresholdPx
557
+ if (locked.value != shouldLock) locked.value = shouldLock
558
+
559
+ val desired = if (shouldLock) thresholdPx else quantize(rawTarget, stepPx)
560
+
561
+ if (proxy.value != desired) {
562
+ proxy.scrollTo(desired.coerceAtLeast(0))
563
+ }
564
+ }
565
+ }
566
+
567
+ val inProgress = remember { mutableStateOf(false) }
568
+ LaunchedEffect(source, thresholdPx) {
569
+ snapshotFlow { isScrollInProgress() }
570
+ .collect { progressing ->
571
+ inProgress.value = if (locked.value) false else progressing
572
+ }
573
+ }
574
+
575
+ return proxy to inProgress.value
576
+ }
577
+
578
+ private const val SCROLLED_PAST_FIRST_ITEM_THRESHOLD = 10_000
579
+
580
+ internal val LocalScrollState = staticCompositionLocalOf<ScrollState> { error("No Scroll State provided") }
581
+ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error("No NavigationOptions provided") }
582
+
583
+ val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
584
+
585
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
586
+ return when (val headerType = options.headerType) {
587
+ is HeaderType.DefaultOrExtended -> when {
588
+ headerType.inputSearchProps == null -> InputSearchType.None
589
+ headerType.useAnimated -> InputSearchType.Animated
590
+ else -> InputSearchType.Header
591
+ }
592
+
593
+ else -> InputSearchType.None
594
+ }
595
+ }
596
+