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