@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,447 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import vn.momo.kits.modifier.InternalApi
4
+
5
+ import androidx.compose.animation.core.animateFloatAsState
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.border
8
+ import androidx.compose.foundation.layout.Arrangement
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.Row
11
+ import androidx.compose.foundation.layout.Spacer
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.offset
15
+ import androidx.compose.foundation.layout.padding
16
+ import androidx.compose.foundation.layout.size
17
+ import androidx.compose.foundation.layout.width
18
+ import androidx.compose.foundation.layout.wrapContentHeight
19
+ import androidx.compose.foundation.shape.RoundedCornerShape
20
+ import androidx.compose.runtime.Composable
21
+ import androidx.compose.runtime.getValue
22
+ import androidx.compose.ui.Alignment
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.draw.clipToBounds
25
+ import androidx.compose.ui.draw.drawBehind
26
+ import androidx.compose.ui.geometry.Offset
27
+ import androidx.compose.ui.graphics.graphicsLayer
28
+ import androidx.compose.ui.graphics.Brush
29
+ import androidx.compose.ui.graphics.Color
30
+ import androidx.compose.ui.layout.onGloballyPositioned
31
+ import androidx.compose.ui.platform.LocalDensity
32
+ import androidx.compose.ui.text.style.TextAlign
33
+ import androidx.compose.ui.unit.Dp
34
+ import androidx.compose.ui.unit.dp
35
+ import vn.momo.kits.components.Icon
36
+ import vn.momo.kits.components.Image
37
+ import vn.momo.kits.components.InputSearchProps
38
+ import vn.momo.kits.const.AppTheme
39
+ import vn.momo.kits.const.Colors
40
+ import vn.momo.kits.components.PopupNotifyProps
41
+ import vn.momo.kits.application.IsShowBaseLineDebug
42
+ import vn.momo.kits.const.Spacing
43
+ import vn.momo.kits.layout.DuoMetrics
44
+ import vn.momo.kits.layout.getScreenLayout
45
+ import vn.momo.kits.modifier.activeOpacityClickable
46
+ import vn.momo.kits.modifier.conditional
47
+ import vn.momo.kits.modifier.setAutomationId
48
+ import vn.momo.kits.navigation.LocalHeaderRightWidthPx
49
+ import vn.momo.kits.navigation.LocalNavigator
50
+ import vn.momo.kits.navigation.LocalOptions
51
+ import vn.momo.kits.navigation.LocalScrollState
52
+ import vn.momo.kits.navigation.getInputSearchType
53
+ import vn.momo.kits.navigation.usesDuoGlassHeader
54
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
55
+
56
+ @InternalApi
57
+ const val HEADER_HEIGHT = 52
58
+ internal enum class InputSearchType { None, Header, Animated }
59
+ enum class TitlePosition { LEFT, CENTER }
60
+
61
+ private val HEADER_ACTIONS_WIDTH = 172.dp
62
+
63
+ // Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
64
+ private val STATUS_BAR_ROW_ACTIONS_EXTRA = 55.dp
65
+
66
+ @Suppress("FrequentlyChangingValue")
67
+ @Composable
68
+ internal fun headerOpacity(): Float {
69
+ val options = LocalOptions.current
70
+ val scrollState = LocalScrollState.current
71
+ val opacityHeight = when (val header = options.headerType) {
72
+ is HeaderType.Animated -> with(LocalDensity.current) { header.layoutOffSet.roundToPx() }
73
+ else -> HEADER_HEIGHT
74
+ }
75
+ val opacity by animateFloatAsState(
76
+ targetValue = when {
77
+ options.headerType is HeaderType.Transparent -> 0f
78
+ opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
79
+ else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
80
+ }
81
+ )
82
+ return opacity
83
+ }
84
+
85
+ // Back button and toolkit share the bar with the title unless the iPhone Duo side bar holds them.
86
+ @Composable
87
+ internal fun headerTitleMaxWidth(): Dp {
88
+ val layout = getScreenLayout()
89
+ val clearance = layout.statusBarClearance
90
+ val clearanceWidth = clearance?.let { it.left + it.right } ?: 0.dp
91
+ if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2 - clearanceWidth
92
+ return layout.width - HEADER_ACTIONS_WIDTH - (if (clearance != null) STATUS_BAR_ROW_ACTIONS_EXTRA else 0.dp) - clearanceWidth
93
+ }
94
+
95
+ // Scroll distance over which the Duo glass bar fades in.
96
+ private val DUO_GLASS_FADE_DISTANCE = 16.dp
97
+
98
+ // RN DuoGlassBackground fallback: translucent surface with a hairline once content scrolls under it.
99
+ @Suppress("FrequentlyChangingValue")
100
+ @Composable
101
+ private fun Modifier.duoGlassBar(): Modifier {
102
+ val scrollState = LocalScrollState.current
103
+ val fadePx = with(LocalDensity.current) { DUO_GLASS_FADE_DISTANCE.toPx() }
104
+ val alpha by animateFloatAsState((scrollState.value / fadePx).coerceIn(0f, 1f), label = "DuoGlassAlpha")
105
+ val colors = AppTheme.current.colors
106
+ return background(colors.background.surface.copy(alpha = 0.8f * alpha))
107
+ .drawBehind {
108
+ val stroke = 0.5.dp.toPx()
109
+ drawLine(
110
+ color = colors.border.default.copy(alpha = alpha),
111
+ start = Offset(0f, size.height - stroke / 2),
112
+ end = Offset(size.width, size.height - stroke / 2),
113
+ strokeWidth = stroke,
114
+ )
115
+ }
116
+ }
117
+
118
+ @Composable
119
+ internal fun Header(onBackHandler: (() -> Unit)? = null) {
120
+ val options = LocalOptions.current
121
+ val navigator = LocalNavigator.current
122
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
123
+ val inputSearchType = getInputSearchType(options)
124
+ val layout = getScreenLayout()
125
+ val useSideBar = layout.sideBar != null
126
+ // iPhone Duo inner portrait: the row sits on the status bar, between its clusters
127
+ val clearance = layout.statusBarClearance
128
+ val statusBarRow = clearance != null && !useSideBar
129
+ val opacity = headerOpacity()
130
+
131
+ val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
132
+
133
+ val animatedAlpha by animateFloatAsState(targetValue = opacity, label = "BackgroundAlpha")
134
+
135
+ val background = if (options.headerType is HeaderType.Transparent)
136
+ options.backgroundColor ?: AppTheme.current.colors.background.surface
137
+ else
138
+ AppTheme.current.colors.background.surface
139
+
140
+ if (options.headerType == HeaderType.None) return
141
+
142
+ val titlePosition = options.titlePosition
143
+ val titleColor = headerColor.tintIconColor
144
+ .copy(alpha = if (inputSearchType == InputSearchType.Animated) 1f - animatedAlpha else 1f)
145
+ // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
146
+ val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
147
+
148
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
149
+ val headerImage = AppTheme.current.assets.headerBackground
150
+ .takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
151
+
152
+ Box(
153
+ Modifier.height(layout.headerHeight)
154
+ .fillMaxWidth()
155
+ .then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
156
+ .conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
157
+ background(background.copy(alpha = animatedAlpha))
158
+ }
159
+ .conditional(IsShowBaseLineDebug) {
160
+ border(1.dp, Colors.blue_03)
161
+ },
162
+ contentAlignment = Alignment.BottomCenter
163
+ ) {
164
+ if (!headerImage.isNullOrEmpty()) {
165
+ Box(
166
+ Modifier
167
+ .matchParentSize()
168
+ .clipToBounds()
169
+ .graphicsLayer { alpha = animatedAlpha }
170
+ .background(background)
171
+ ) {
172
+ Image(
173
+ source = headerImage,
174
+ loading = false,
175
+ modifier = Modifier
176
+ .fillMaxWidth()
177
+ .wrapContentHeight(Alignment.Top, unbounded = true)
178
+ .height(HEADER_BACKGROUND_HEIGHT),
179
+ )
180
+ }
181
+ }
182
+ Row(
183
+ modifier = Modifier.height(layout.headerBarHeight)
184
+ .fillMaxWidth()
185
+ .horizontalSafeAreaPadding()
186
+ .padding(
187
+ start = Spacing.M + (clearance?.left ?: 0.dp),
188
+ end = Spacing.M + (clearance?.right ?: 0.dp),
189
+ ),
190
+ verticalAlignment = Alignment.CenterVertically,
191
+ horizontalArrangement = Arrangement.SpaceBetween
192
+ ) {
193
+ if (!options.hiddenBack && !useSideBar) {
194
+ if (statusBarRow) {
195
+ SideBarButton(
196
+ icon = "arrow-back",
197
+ headerColor = headerColor,
198
+ automationId = "btn_navigation_back",
199
+ onClick = { navigator.onBackSafe { } },
200
+ )
201
+ } else {
202
+ BackButton(
203
+ borderColor = headerColor.borderColor,
204
+ backgroundButton = headerColor.backgroundButton,
205
+ tintIconColor = headerColor.tintIconColor,
206
+ onBackHandler = {
207
+ navigator.onBackSafe { }
208
+ }
209
+ )
210
+ }
211
+ Spacer(Modifier.width(Spacing.S))
212
+ }
213
+
214
+ if (centerTitle) {
215
+ Spacer(Modifier.weight(1f))
216
+ } else {
217
+ HeaderContent(
218
+ headerTitle = options.headerTitle,
219
+ tintIconColor = titleColor,
220
+ titlePosition = titlePosition,
221
+ modifier = Modifier.weight(1f)
222
+ )
223
+ }
224
+ Box(Modifier.onGloballyPositioned {
225
+ if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
226
+ }){
227
+ val headerRight = options.headerRight
228
+ when {
229
+ useSideBar && headerRight is HeaderRight.Toolkit -> Unit
230
+ statusBarRow && headerRight is HeaderRight.Toolkit -> Toolkit(
231
+ headerRight = headerRight,
232
+ tintColor = options.tintColor,
233
+ headerColor = headerColor,
234
+ layout = ToolkitLayout.SideBarRow,
235
+ )
236
+ else -> HeaderRight(headerRight, options.tintColor, headerColor)
237
+ }
238
+ }
239
+ }
240
+ if (centerTitle) {
241
+ // on the status bar row the clusters are lopsided, so both sides keep the wider one's room
242
+ val centerInset = if (clearance != null) {
243
+ val back = if (options.hiddenBack || useSideBar) 0.dp else DuoMetrics.controlSize + Spacing.S
244
+ val right = with(LocalDensity.current) { headerRightWidthPx.intValue.toDp() }
245
+ maxOf(clearance.left + back, clearance.right + right) + Spacing.S
246
+ } else {
247
+ 0.dp
248
+ }
249
+ Box(
250
+ modifier = Modifier.height(layout.headerBarHeight)
251
+ .fillMaxWidth()
252
+ .horizontalSafeAreaPadding()
253
+ .padding(horizontal = Spacing.M + centerInset),
254
+ contentAlignment = Alignment.Center
255
+ ) {
256
+ HeaderContent(
257
+ headerTitle = options.headerTitle,
258
+ tintIconColor = titleColor,
259
+ titlePosition = titlePosition,
260
+ centerWidthFraction = if (clearance != null) 1f else 0.5f,
261
+ modifier = Modifier.fillMaxWidth()
262
+ )
263
+ }
264
+ }
265
+ VerticalShadow(opacity)
266
+ }
267
+ }
268
+
269
+ @Composable
270
+ private fun BackButton(borderColor: Color, backgroundButton: Color, tintIconColor: Color, onBackHandler: () -> Unit){
271
+ Box(
272
+ modifier = Modifier
273
+ .size(28.dp)
274
+ .background(backgroundButton, RoundedCornerShape(100))
275
+ .border(width = 0.2.dp, color = borderColor, shape = RoundedCornerShape(100))
276
+ .activeOpacityClickable(onClick = onBackHandler)
277
+ .padding(Spacing.XS)
278
+ .setAutomationId("btn_navigation_back"),
279
+ contentAlignment = Alignment.Center
280
+ ) {
281
+ Icon(
282
+ source = "arrow-back",
283
+ color = tintIconColor,
284
+ size = 20.dp,
285
+ )
286
+ }
287
+ }
288
+
289
+ @Composable
290
+ internal fun HeaderContent(
291
+ headerTitle: HeaderTitle,
292
+ tintIconColor: Color,
293
+ titlePosition: TitlePosition = TitlePosition.LEFT,
294
+ centerWidthFraction: Float = 0.5f,
295
+ modifier: Modifier = Modifier,
296
+ ){
297
+ Box(
298
+ modifier = modifier,
299
+ contentAlignment = if (titlePosition == TitlePosition.CENTER) Alignment.Center else Alignment.CenterStart
300
+ ) {
301
+ when (headerTitle){
302
+ is HeaderTitle.Default -> {
303
+ HeaderTitle(
304
+ title = headerTitle.title,
305
+ color = tintIconColor,
306
+ modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
307
+ textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
308
+ )
309
+ }
310
+ is HeaderTitle.Journey -> {
311
+ HeaderJourney(
312
+ data = headerTitle,
313
+ tintIconColor = tintIconColor
314
+ )
315
+ }
316
+ is HeaderTitle.Location -> {
317
+ HeaderLocation(
318
+ data = headerTitle,
319
+ tintIconColor = tintIconColor
320
+ )
321
+ }
322
+ is HeaderTitle.User -> {
323
+ HeaderUser(
324
+ data = headerTitle
325
+ )
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ @Composable
332
+ internal fun VerticalShadow(opacity: Float){
333
+ if(opacity == 1f){
334
+ Box(modifier = Modifier
335
+ .fillMaxWidth()
336
+ .height(6.dp)
337
+ .offset(x = 0.dp, y = 6.dp)
338
+ .background(
339
+ brush = Brush.verticalGradient(
340
+ colors = listOf(Color.Black.copy(alpha = 0.05f), Color.Transparent)
341
+ )
342
+ )
343
+ )
344
+ }
345
+ }
346
+
347
+ sealed class HeaderTitle {
348
+ class Default(val title: String) : HeaderTitle()
349
+
350
+ class User(
351
+ val title: String,
352
+ val subTitle: String? = null,
353
+ val image: List<String>? = null,
354
+ val dotColor: Color? = null,
355
+ val tintColor: Color? = null,
356
+ val onPress: (() -> Unit)? = null,
357
+ val icons: List<String> = emptyList(),
358
+ val isLoading: Boolean = false
359
+ ) : HeaderTitle()
360
+
361
+ class Location(
362
+ val description: String? = null,
363
+ val location: String,
364
+ val tintColor: String? = null,
365
+ val onPress: (() -> Unit)? = null,
366
+ val isLoading: Boolean = false
367
+ ) : HeaderTitle()
368
+
369
+ class Journey(
370
+ val start: String,
371
+ val end: String? = null,
372
+ val description: String? = null,
373
+ val icon: String,
374
+ val iconColor: String? = null,
375
+ val tintColor: String? = null,
376
+ val onPress: (() -> Unit)? = null,
377
+ val isLoading: Boolean = false
378
+ ) : HeaderTitle()
379
+ }
380
+
381
+ sealed class HeaderType {
382
+ interface DefaultOrExtended {
383
+ val useAnimated: Boolean
384
+ val inputSearchProps: InputSearchProps?
385
+ }
386
+
387
+ data class Default(
388
+ override val useAnimated: Boolean = false,
389
+ override val inputSearchProps: InputSearchProps? = null
390
+ ) : HeaderType(), DefaultOrExtended
391
+
392
+ data class Extended(
393
+ override val useAnimated: Boolean = false,
394
+ override val inputSearchProps: InputSearchProps? = null
395
+ ) : HeaderType(), DefaultOrExtended
396
+
397
+ data object None : HeaderType()
398
+
399
+ data class Animated(
400
+ val aspectRatio: AnimatedHeaderRatio = AnimatedHeaderRatio.RATIO_16_9,
401
+ val isSurface: Boolean = true,
402
+ val layoutOffSet: Dp = 56.dp,
403
+ val height: Dp? = null,
404
+ val composable: @Composable (scrollState: Int) -> Unit = {}
405
+ ) : HeaderType()
406
+
407
+ data class Transparent(
408
+ val isFullScreenContent: Boolean = false
409
+ ) : HeaderType()
410
+ }
411
+
412
+ data class HeaderBackProps(
413
+ val preventBack: PopupNotifyProps? = null,
414
+ )
415
+
416
+ @InternalApi
417
+ data class HeaderColor(val tintIconColor: Color, val backgroundButton: Color, val borderColor: Color)
418
+ internal fun getHeaderColor(headerType: HeaderType, opacity: Float, tintColor: Color?, defaultColor: Color): HeaderColor{
419
+ return if(headerType is HeaderType.Animated) {
420
+ if (opacity == 1f || !headerType.isSurface)
421
+ HeaderColor(
422
+ tintIconColor = Colors.black_17,
423
+ backgroundButton = Colors.black_01.copy(alpha = 0.6f),
424
+ borderColor = Colors.black_20.copy(alpha = 0.2f)
425
+ )
426
+ else
427
+ HeaderColor(
428
+ tintIconColor = Colors.black_01,
429
+ backgroundButton = Colors.black_20.copy(alpha = 0.6f),
430
+ borderColor = Color.Transparent
431
+ )
432
+ }
433
+ else {
434
+ if (tintColor == Colors.black_01)
435
+ HeaderColor(
436
+ tintIconColor = tintColor,
437
+ backgroundButton = Colors.black_20.copy(alpha = 0.6f),
438
+ borderColor = Colors.black_01.copy(alpha = 0.2f)
439
+ )
440
+ else
441
+ HeaderColor(
442
+ tintIconColor = tintColor ?: defaultColor,
443
+ backgroundButton = Colors.black_01.copy(alpha = 0.6f),
444
+ borderColor = Colors.black_20.copy(alpha = 0.2f)
445
+ )
446
+ }
447
+ }
@@ -0,0 +1,103 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.animation.core.animateFloatAsState
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxHeight
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.fillMaxWidth
9
+ import androidx.compose.foundation.layout.height
10
+ import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.getValue
12
+ import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.draw.clipToBounds
14
+ import androidx.compose.ui.graphics.graphicsLayer
15
+ import androidx.compose.ui.graphics.Brush
16
+ import androidx.compose.ui.graphics.Color
17
+ import androidx.compose.ui.platform.LocalDensity
18
+ import androidx.compose.ui.unit.dp
19
+ import vn.momo.kits.components.Image
20
+ import vn.momo.kits.const.AppTheme
21
+ import vn.momo.kits.layout.getScreenLayout
22
+ import vn.momo.kits.modifier.conditional
23
+ import vn.momo.kits.navigation.LocalOptions
24
+ import vn.momo.kits.navigation.LocalScrollState
25
+ import vn.momo.kits.navigation.getInputSearchType
26
+ import vn.momo.kits.navigation.usesDuoGlassHeader
27
+
28
+ enum class AnimatedHeaderRatio(val value: Float){
29
+ RATIO_16_9(16f / 9f),
30
+ RATIO_1_1(1f),
31
+ RATIO_3_2(3f / 2f)
32
+ }
33
+
34
+ internal val HEADER_BACKGROUND_HEIGHT = 154.dp
35
+
36
+ // RN DuoScrollGradient: extended gradient runs this far below the header
37
+ private val DUO_GRADIENT_EXTRA = 52.dp
38
+
39
+ @Suppress("FrequentlyChangingValue")
40
+ @Composable
41
+ internal fun HeaderBackground() {
42
+ val density = LocalDensity.current
43
+ val theme = AppTheme.current
44
+ val options = LocalOptions.current
45
+ val scrollState = LocalScrollState.current
46
+ val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
47
+
48
+ val layout = getScreenLayout()
49
+ val minHeight = layout.headerHeight
50
+ val maxHeight = HEADER_BACKGROUND_HEIGHT
51
+ val opacity by animateFloatAsState(
52
+ targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
53
+ )
54
+
55
+ val color = if (opacity == 0f) backgroundColor else Color(0xFFFDCADE)
56
+ val height = when (options.headerType) {
57
+ is HeaderType.Default -> minHeight
58
+ is HeaderType.Extended -> {
59
+ if (AppTheme.current.isHeaderImage()){
60
+ if (opacity == 0f) minHeight else maxHeight
61
+ } else {
62
+ maxHeight
63
+ }
64
+ }
65
+ else -> 0.dp
66
+ }
67
+
68
+ // iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
69
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
70
+ // RN HeaderExtendHeader with search on the side bar layout: the gradient fills the header box,
71
+ // which for extended shrinks from 52pt below the header over the first 100pt of scroll
72
+ val duoSearch = layout.sideBar != null && getInputSearchType(options) != InputSearchType.None
73
+ val fillsBox = useDuoGlass || duoSearch
74
+ val duoModifier = when {
75
+ useDuoGlass -> {
76
+ val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
77
+ Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
78
+ }
79
+ duoSearch && options.headerType is HeaderType.Extended -> {
80
+ val progress = (with(density) { scrollState.value.toDp() } / 100.dp).coerceIn(0f, 1f)
81
+ Modifier.height(minHeight + DUO_GRADIENT_EXTRA * (1 - progress))
82
+ }
83
+ duoSearch -> Modifier.height(minHeight)
84
+ else -> Modifier.height(height)
85
+ }
86
+
87
+ Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
88
+ // on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
89
+ Box(Modifier
90
+ .fillMaxWidth()
91
+ .then(if (fillsBox && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
92
+ .conditional(!theme.isHeaderImage()) {
93
+ background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
94
+ }
95
+ ){
96
+ if (!theme.isHeaderImage()) return@Box
97
+ Image(
98
+ source = theme.assets.headerBackground!!,
99
+ modifier = Modifier.fillMaxSize(),
100
+ )
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,106 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.foundation.layout.Arrangement
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.Row
7
+ import androidx.compose.foundation.layout.Spacer
8
+ import androidx.compose.foundation.layout.fillMaxWidth
9
+ import androidx.compose.foundation.layout.height
10
+ import androidx.compose.foundation.layout.width
11
+ import androidx.compose.foundation.layout.widthIn
12
+ import androidx.compose.foundation.shape.RoundedCornerShape
13
+ import androidx.compose.runtime.Composable
14
+ import androidx.compose.ui.Alignment
15
+ import androidx.compose.ui.Modifier
16
+ import androidx.compose.ui.draw.clip
17
+ import androidx.compose.ui.graphics.Color
18
+ import androidx.compose.ui.text.style.TextOverflow
19
+ import androidx.compose.ui.unit.dp
20
+ import vn.momo.kits.components.Icon
21
+ import vn.momo.kits.components.Skeleton
22
+ import vn.momo.kits.components.Text
23
+ import vn.momo.kits.const.Spacing
24
+ import vn.momo.kits.const.Typography
25
+ import vn.momo.kits.modifier.activeOpacityClickable
26
+
27
+ @Composable
28
+ internal fun HeaderJourney(
29
+ data: HeaderTitle,
30
+ tintIconColor: Color,
31
+ ) {
32
+ if (data !is HeaderTitle.Journey) return
33
+ if (data.isLoading) return TitleJourneyShimmer()
34
+
35
+ val maxWidth = headerTitleMaxWidth()
36
+ val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
37
+ val iconTint = parseHeaderTint(data.iconColor) ?: tint
38
+
39
+ Column(
40
+ modifier = Modifier
41
+ .widthIn(max = maxWidth)
42
+ .activeOpacityClickable(enabled = data.onPress != null, onClick = { data.onPress?.invoke() }),
43
+ verticalArrangement = Arrangement.Center,
44
+ ) {
45
+ Row(verticalAlignment = Alignment.CenterVertically) {
46
+ Text(
47
+ text = data.start,
48
+ color = tint,
49
+ style = Typography.actionXsBold,
50
+ maxLines = 1,
51
+ overflow = TextOverflow.Ellipsis,
52
+ modifier = Modifier.weight(1f, fill = false),
53
+ )
54
+ Spacer(Modifier.width(Spacing.S))
55
+ if (data.icon.isNotBlank()) {
56
+ Icon(source = data.icon, size = 16.dp, color = iconTint)
57
+ } else {
58
+ Spacer(Modifier.width(16.dp))
59
+ }
60
+ Spacer(Modifier.width(Spacing.S))
61
+ if (!data.end.isNullOrEmpty()) {
62
+ Text(
63
+ text = data.end,
64
+ color = tint,
65
+ style = Typography.actionXsBold,
66
+ maxLines = 1,
67
+ overflow = TextOverflow.Ellipsis,
68
+ modifier = Modifier.weight(1f, fill = false),
69
+ )
70
+ }
71
+ Spacer(Modifier.width(Spacing.XS))
72
+ Icon(source = "16_arrow_chevron_down_small", size = 16.dp, color = tint)
73
+ }
74
+ if (!data.description.isNullOrEmpty()) {
75
+ Text(
76
+ text = data.description,
77
+ color = tint,
78
+ style = Typography.descriptionXsRegular,
79
+ maxLines = 1,
80
+ overflow = TextOverflow.Ellipsis,
81
+ )
82
+ }
83
+ }
84
+ }
85
+
86
+ @Composable
87
+ private fun TitleJourneyShimmer() {
88
+ Column(modifier = Modifier.fillMaxWidth()) {
89
+ Box(modifier = Modifier.height(18.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
90
+ Spacer(Modifier.height(Spacing.XXS))
91
+ Box(modifier = Modifier.height(12.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
92
+ }
93
+ }
94
+
95
+ internal fun parseHeaderTint(value: String?): Color? {
96
+ val raw = value?.trim()?.removePrefix("#") ?: return null
97
+ val hex = when (raw.length) {
98
+ 3 -> "FF" + raw.map { "$it$it" }.joinToString("")
99
+ 6 -> "FF$raw"
100
+ // CSS #RRGGBBAA, as RN passes it; Compose wants AARRGGBB
101
+ 8 -> raw.substring(6) + raw.substring(0, 6)
102
+ else -> return null
103
+ }
104
+ val v = hex.toLongOrNull(16) ?: return null
105
+ return Color(v)
106
+ }