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