@momo-kits/native-kits 0.164.1-beta.9-debug → 0.164.1-headerconfig.1-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/settings.local.json +7 -178
  2. package/CLAUDE.md +2 -0
  3. package/compose/build.gradle.kts +1 -1
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +3 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -1
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +18 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +30 -4
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +3 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +8 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +9 -4
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +4 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +3 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +104 -22
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -9
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +8 -4
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +2 -1
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +38 -5
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +20 -11
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +10 -23
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +20 -23
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +11 -27
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +4 -10
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +19 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +41 -3
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +3 -1
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +3 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +5 -1
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +5 -8
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -1
  39. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  40. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +6 -3
  41. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +8 -23
  42. package/gradle/libs.versions.toml +1 -1
  43. package/gradle.properties +1 -1
  44. package/ios/Application/Navigation/NavigatorManager.swift +0 -27
  45. package/ios/Colors+Radius+Spacing/Theme.swift +2 -2
  46. package/ios/Typography/KitDesignSystem.swift +17 -0
  47. package/ios/Typography/Text.swift +25 -1
  48. package/ios/native-kits.podspec +1 -1
  49. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  50. package/local.properties +2 -2
  51. package/package.json +1 -1
  52. package/publish.sh +25 -0
  53. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ComposeBackGesture.ios.kt +0 -65
@@ -18,9 +18,7 @@ import androidx.compose.runtime.Stable
18
18
  import androidx.compose.runtime.derivedStateOf
19
19
  import androidx.compose.runtime.getValue
20
20
  import androidx.compose.runtime.mutableStateMapOf
21
- import androidx.compose.runtime.mutableStateOf
22
21
  import androidx.compose.runtime.remember
23
- import androidx.compose.runtime.setValue
24
22
  import androidx.compose.runtime.staticCompositionLocalOf
25
23
  import androidx.compose.ui.Modifier
26
24
  import androidx.compose.ui.draw.drawBehind
@@ -44,21 +42,14 @@ private const val SCRIM_MAX_ALPHA = 0.6f
44
42
  private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
45
43
  private val EDGE_SHADOW_WIDTH = 8.dp
46
44
 
47
- /**
48
- * Kill switch for the swipe-back gesture. Set to `false` to make kit back handling always win,
49
- * which disables the predictive-back drag on both platforms — the gesture then pops without
50
- * following the finger, and fires no haptic. Screen transitions stay on the parallax curve either
51
- * way.
52
- */
45
+ /** Kill switch: `false` restores pre-DS-760 back handling without a kit release. Transitions
46
+ * stay on the new curve either way. */
53
47
  object SwipeBackConfig {
54
- // Snapshot-backed so a host toggle takes effect on the next frame instead of waiting for an
55
- // unrelated recomposition of StackScreen.
56
- var enabled: Boolean by mutableStateOf(true)
48
+ var enabled: Boolean = true
57
49
  }
58
50
 
59
- // LinearEasing is mandatory, not a style choice: SeekableTransitionState.seekTo() maps the drag
60
- // fraction straight onto the animation timeline, so any curve would decouple the card from the
61
- // finger mid-gesture.
51
+ // LinearEasing is mandatory: seekTo() maps drag fraction onto the timeline, so a curve would
52
+ // decouple the card from the finger.
62
53
  private val stackSpec = tween<IntOffset>(STACK_TRANSITION_DURATION, easing = LinearEasing)
63
54
 
64
55
  internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackEnter(): EnterTransition =
@@ -76,13 +67,9 @@ internal fun AnimatedContentTransitionScope<NavBackStackEntry>.stackPopExit(): E
76
67
  slideOutHorizontally(stackSpec) { it }
77
68
 
78
69
  /**
79
- * Tracks which screen ids are currently composed so a screen can tell whether it is the top card
80
- * or the one underneath.
81
- *
82
- * Screen ids come from a single monotonic process-wide counter and NavHost keeps both entries
83
- * composed for the whole transition segment, so "highest composed stack id" is stable in both
84
- * directions. `currentBackStackEntry` is not usable here: it flips at frame 0 of a pop, which
85
- * would make the scrim vanish instantly instead of fading.
70
+ * Which composed screen is the top card. Ids are monotonic and NavHost keeps both entries composed
71
+ * for the whole segment, so "highest id" is stable both ways. `currentBackStackEntry` is not: it
72
+ * flips at frame 0 of a pop and the scrim would vanish instead of fading.
86
73
  */
87
74
  @Stable
88
75
  internal class StackTransitionCoordinator {
@@ -111,10 +98,8 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
111
98
  content: @Composable () -> Unit,
112
99
  ) {
113
100
  val coordinator = LocalStackTransition.current
114
- // attach() writes state that derivedStateOf { topStackId } reads in the same pass, so it has to
115
- // stay out of the composable body.
116
- DisposableEffect(coordinator, id, isStackRoute) {
117
- coordinator.attach(id, isStackRoute)
101
+ coordinator.attach(id, isStackRoute)
102
+ DisposableEffect(coordinator, id) {
118
103
  onDispose { coordinator.detach(id) }
119
104
  }
120
105
 
@@ -147,8 +132,7 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
147
132
  Box(
148
133
  Modifier
149
134
  .matchParentSize()
150
- // Both alphas are read inside drawBehind so a drag only re-draws; reading them
151
- // in the composable body would recompose the whole screen every gesture frame.
135
+ // read inside drawBehind: in the body this would recompose every gesture frame
152
136
  .drawBehind {
153
137
  drawRect(
154
138
  brush = shadowBrush,
@@ -7,19 +7,13 @@ import vn.momo.kits.platform.getScreenDimensions
7
7
  import vn.momo.kits.platform.rememberBackGestureProgress
8
8
  import vn.momo.maxapi.IMaxApi
9
9
 
10
- // RN fires the tick at a flat 100 px of thumb travel; keep it unscaled so the physical distance is
11
- // the same on every device.
12
- private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 100f
10
+ // Unscaled, so the tick lands at the same physical distance on every device.
11
+ private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
13
12
 
14
- /**
15
- * Fires one light tick once the back gesture has travelled ~100dp, re-arming when the finger drops
16
- * back below the threshold. iOS only — [rememberBackGestureProgress] is always `null` on Android.
17
- */
13
+ /** One light tick per crossing, re-armed below the threshold. iOS only. */
18
14
  @Composable
19
15
  internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
20
- // With the kill switch off the kit's own BackHandler swallows the gesture, so the card never
21
- // moves — a tick there would be a phantom.
22
- if (maxApi == null || !SwipeBackConfig.enabled) return
16
+ if (maxApi == null) return
23
17
 
24
18
  val progress = rememberBackGestureProgress()
25
19
  val screenWidth = getScreenDimensions().width.toFloat()
@@ -8,12 +8,14 @@ import androidx.compose.animation.scaleOut
8
8
  import androidx.compose.foundation.background
9
9
  import androidx.compose.foundation.layout.*
10
10
  import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.DisposableEffect
11
12
  import androidx.compose.runtime.LaunchedEffect
12
13
  import androidx.compose.ui.Alignment
13
14
  import androidx.compose.ui.Modifier
14
15
  import androidx.compose.ui.unit.dp
15
16
  import androidx.navigation.compose.NavHost
16
17
  import androidx.navigation.compose.composable
18
+ import androidx.navigation.compose.currentBackStackEntryAsState
17
19
  import androidx.navigation.compose.rememberNavController
18
20
  import vn.momo.kits.const.AppNavigationBar
19
21
  import vn.momo.kits.const.AppTheme
@@ -24,6 +26,7 @@ import vn.momo.kits.navigation.LocalNavigator
24
26
  import vn.momo.kits.navigation.NavigationOptions
25
27
  import vn.momo.kits.navigation.StackScreen
26
28
  import vn.momo.kits.navigation.component.HeaderType
29
+ import vn.momo.kits.platform.reportFloatingArrowBack
27
30
 
28
31
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
29
32
  internal fun setBottomTabOption(index: Int, options: NavigationOptions){
@@ -59,6 +62,22 @@ fun BottomTab(
59
62
  )
60
63
  }
61
64
 
65
+ // Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
66
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
67
+ val isFirstTab = currentRoute == null || currentRoute == "option0"
68
+
69
+ DisposableEffect(isFirstTab) {
70
+ reportFloatingArrowBack(!isFirstTab)
71
+ navigator.tabBackHandler = if (isFirstTab) null else ({
72
+ navController.popBackStack(navController.graph.startDestinationRoute!!, false)
73
+ })
74
+ // both live outside this composition
75
+ onDispose {
76
+ reportFloatingArrowBack(false)
77
+ navigator.tabBackHandler = null
78
+ }
79
+ }
80
+
62
81
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
63
82
  Box(modifier = Modifier
64
83
  .fillMaxSize()
@@ -15,11 +15,14 @@ import androidx.compose.foundation.layout.offset
15
15
  import androidx.compose.foundation.layout.padding
16
16
  import androidx.compose.foundation.layout.size
17
17
  import androidx.compose.foundation.layout.width
18
+ import androidx.compose.foundation.layout.wrapContentHeight
18
19
  import androidx.compose.foundation.shape.RoundedCornerShape
19
20
  import androidx.compose.runtime.Composable
20
21
  import androidx.compose.runtime.getValue
21
22
  import androidx.compose.ui.Alignment
22
23
  import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.draw.clipToBounds
25
+ import androidx.compose.ui.graphics.graphicsLayer
23
26
  import androidx.compose.ui.graphics.Brush
24
27
  import androidx.compose.ui.graphics.Color
25
28
  import androidx.compose.ui.layout.onGloballyPositioned
@@ -28,6 +31,7 @@ import androidx.compose.ui.text.style.TextAlign
28
31
  import androidx.compose.ui.unit.Dp
29
32
  import androidx.compose.ui.unit.dp
30
33
  import vn.momo.kits.components.Icon
34
+ import vn.momo.kits.components.Image
31
35
  import vn.momo.kits.components.InputSearchProps
32
36
  import vn.momo.kits.const.AppStatusBar
33
37
  import vn.momo.kits.const.AppTheme
@@ -88,15 +92,38 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
88
92
  // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
89
93
  val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
90
94
 
95
+ val headerImage = AppTheme.current.assets.headerBackground
96
+ .takeUnless { options.headerType is HeaderType.Transparent }
97
+
91
98
  Box(
92
99
  Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
93
100
  .fillMaxWidth()
94
- .background(background.copy(alpha = animatedAlpha))
101
+ .conditional(headerImage.isNullOrEmpty()) {
102
+ background(background.copy(alpha = animatedAlpha))
103
+ }
95
104
  .conditional(IsShowBaseLineDebug) {
96
105
  border(1.dp, Colors.blue_03)
97
106
  },
98
107
  contentAlignment = Alignment.BottomCenter
99
108
  ) {
109
+ if (!headerImage.isNullOrEmpty()) {
110
+ Box(
111
+ Modifier
112
+ .matchParentSize()
113
+ .clipToBounds()
114
+ .graphicsLayer { alpha = animatedAlpha }
115
+ .background(background)
116
+ ) {
117
+ Image(
118
+ source = headerImage,
119
+ loading = false,
120
+ modifier = Modifier
121
+ .fillMaxWidth()
122
+ .wrapContentHeight(Alignment.Top, unbounded = true)
123
+ .height(HEADER_BACKGROUND_HEIGHT),
124
+ )
125
+ }
126
+ }
100
127
  Row(
101
128
  modifier = Modifier.height(HEADER_HEIGHT.dp)
102
129
  .fillMaxWidth()
@@ -191,8 +218,18 @@ internal fun HeaderContent(
191
218
  textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
192
219
  )
193
220
  }
194
- is HeaderTitle.Journey -> {}
195
- is HeaderTitle.Location -> {}
221
+ is HeaderTitle.Journey -> {
222
+ HeaderJourney(
223
+ data = headerTitle,
224
+ tintIconColor = tintIconColor
225
+ )
226
+ }
227
+ is HeaderTitle.Location -> {
228
+ HeaderLocation(
229
+ data = headerTitle,
230
+ tintIconColor = tintIconColor
231
+ )
232
+ }
196
233
  is HeaderTitle.User -> {
197
234
  HeaderUser(
198
235
  data = headerTitle
@@ -274,6 +311,7 @@ sealed class HeaderType {
274
311
  val aspectRatio: AnimatedHeaderRatio = AnimatedHeaderRatio.RATIO_16_9,
275
312
  val isSurface: Boolean = true,
276
313
  val layoutOffSet: Dp = 56.dp,
314
+ val height: Dp? = null,
277
315
  val composable: @Composable (scrollState: Int) -> Unit = {}
278
316
  ) : HeaderType()
279
317
 
@@ -27,6 +27,8 @@ enum class AnimatedHeaderRatio(val value: Float){
27
27
  RATIO_3_2(3f / 2f)
28
28
  }
29
29
 
30
+ internal val HEADER_BACKGROUND_HEIGHT = 154.dp
31
+
30
32
  @Suppress("FrequentlyChangingValue")
31
33
  @Composable
32
34
  internal fun HeaderBackground() {
@@ -37,7 +39,7 @@ internal fun HeaderBackground() {
37
39
  val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
38
40
 
39
41
  val minHeight = AppStatusBar.current + HEADER_HEIGHT.dp
40
- val maxHeight = 154.dp
42
+ val maxHeight = HEADER_BACKGROUND_HEIGHT
41
43
  val opacity by animateFloatAsState(
42
44
  targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
43
45
  )
@@ -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
+ import vn.momo.kits.platform.getScreenDimensions
27
+
28
+ @Composable
29
+ internal fun HeaderJourney(
30
+ data: HeaderTitle,
31
+ tintIconColor: Color,
32
+ ) {
33
+ if (data !is HeaderTitle.Journey) return
34
+ if (data.isLoading) return TitleJourneyShimmer()
35
+
36
+ val maxWidth = getScreenDimensions().width.dp - 172.dp
37
+ val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
38
+ val iconTint = parseHeaderTint(data.iconColor) ?: tint
39
+
40
+ Column(
41
+ modifier = Modifier
42
+ .widthIn(max = maxWidth)
43
+ .activeOpacityClickable(enabled = data.onPress != null, onClick = { data.onPress?.invoke() }),
44
+ verticalArrangement = Arrangement.Center,
45
+ ) {
46
+ Row(verticalAlignment = Alignment.CenterVertically) {
47
+ Text(
48
+ text = data.start,
49
+ color = tint,
50
+ style = Typography.actionXsBold,
51
+ maxLines = 1,
52
+ overflow = TextOverflow.Ellipsis,
53
+ modifier = Modifier.weight(1f, fill = false),
54
+ )
55
+ Spacer(Modifier.width(Spacing.S))
56
+ if (data.icon.isNotBlank()) {
57
+ Icon(source = data.icon, size = 16.dp, color = iconTint)
58
+ } else {
59
+ Spacer(Modifier.width(16.dp))
60
+ }
61
+ Spacer(Modifier.width(Spacing.S))
62
+ if (!data.end.isNullOrEmpty()) {
63
+ Text(
64
+ text = data.end,
65
+ color = tint,
66
+ style = Typography.actionXsBold,
67
+ maxLines = 1,
68
+ overflow = TextOverflow.Ellipsis,
69
+ modifier = Modifier.weight(1f, fill = false),
70
+ )
71
+ }
72
+ Spacer(Modifier.width(Spacing.XS))
73
+ Icon(source = "16_arrow_chevron_down_small", size = 16.dp, color = tint)
74
+ }
75
+ if (!data.description.isNullOrEmpty()) {
76
+ Text(
77
+ text = data.description,
78
+ color = tint,
79
+ style = Typography.descriptionXsRegular,
80
+ maxLines = 1,
81
+ overflow = TextOverflow.Ellipsis,
82
+ )
83
+ }
84
+ }
85
+ }
86
+
87
+ @Composable
88
+ private fun TitleJourneyShimmer() {
89
+ Column(modifier = Modifier.fillMaxWidth()) {
90
+ Box(modifier = Modifier.height(18.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
91
+ Spacer(Modifier.height(Spacing.XXS))
92
+ Box(modifier = Modifier.height(12.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
93
+ }
94
+ }
95
+
96
+ internal fun parseHeaderTint(value: String?): Color? {
97
+ val raw = value?.trim()?.removePrefix("#") ?: return null
98
+ val hex = when (raw.length) {
99
+ 3 -> "FF" + raw.map { "$it$it" }.joinToString("")
100
+ 6 -> "FF$raw"
101
+ 8 -> raw
102
+ else -> return null
103
+ }
104
+ val v = hex.toLongOrNull(16) ?: return null
105
+ return Color(v)
106
+ }
@@ -0,0 +1,75 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.foundation.layout.Box
4
+ import androidx.compose.foundation.layout.Column
5
+ import androidx.compose.foundation.layout.Row
6
+ import androidx.compose.foundation.layout.Spacer
7
+ import androidx.compose.foundation.layout.fillMaxWidth
8
+ import androidx.compose.foundation.layout.height
9
+ import androidx.compose.foundation.layout.width
10
+ import androidx.compose.foundation.layout.widthIn
11
+ import androidx.compose.foundation.shape.RoundedCornerShape
12
+ import androidx.compose.runtime.Composable
13
+ import androidx.compose.ui.Alignment
14
+ import androidx.compose.ui.Modifier
15
+ import androidx.compose.ui.draw.clip
16
+ import androidx.compose.ui.graphics.Color
17
+ import androidx.compose.ui.text.style.TextOverflow
18
+ import androidx.compose.ui.unit.dp
19
+ import vn.momo.kits.components.Icon
20
+ import vn.momo.kits.components.Skeleton
21
+ import vn.momo.kits.components.Text
22
+ import vn.momo.kits.const.Spacing
23
+ import vn.momo.kits.const.Typography
24
+ import vn.momo.kits.modifier.activeOpacityClickable
25
+ import vn.momo.kits.platform.getScreenDimensions
26
+
27
+ @Composable
28
+ internal fun HeaderLocation(
29
+ data: HeaderTitle,
30
+ tintIconColor: Color,
31
+ ) {
32
+ if (data !is HeaderTitle.Location) return
33
+ if (data.isLoading) return TitleLocationShimmer()
34
+
35
+ val maxWidth = getScreenDimensions().width.dp - 172.dp
36
+ val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
37
+
38
+ Column(
39
+ modifier = Modifier
40
+ .widthIn(max = maxWidth)
41
+ .activeOpacityClickable(enabled = data.onPress != null, onClick = { data.onPress?.invoke() }),
42
+ horizontalAlignment = Alignment.Start,
43
+ ) {
44
+ if (!data.description.isNullOrEmpty()) {
45
+ Text(
46
+ text = data.description,
47
+ color = tint,
48
+ style = Typography.descriptionXsRegular,
49
+ maxLines = 1,
50
+ overflow = TextOverflow.Ellipsis,
51
+ )
52
+ }
53
+ Row(verticalAlignment = Alignment.CenterVertically) {
54
+ Text(
55
+ text = data.location,
56
+ color = tint,
57
+ style = Typography.actionXsBold,
58
+ maxLines = 1,
59
+ overflow = TextOverflow.Ellipsis,
60
+ modifier = Modifier.weight(1f, fill = false),
61
+ )
62
+ Spacer(Modifier.width(Spacing.XS))
63
+ Icon(source = "16_arrow_chevron_down_small", size = 16.dp, color = tint)
64
+ }
65
+ }
66
+ }
67
+
68
+ @Composable
69
+ private fun TitleLocationShimmer() {
70
+ Column(modifier = Modifier.fillMaxWidth()) {
71
+ Box(modifier = Modifier.height(12.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
72
+ Spacer(Modifier.height(Spacing.XXS))
73
+ Box(modifier = Modifier.height(18.dp).width(120.dp).clip(RoundedCornerShape(8.dp))) { Skeleton() }
74
+ }
75
+ }
@@ -12,6 +12,8 @@ import vn.momo.kits.components.Text
12
12
  import vn.momo.kits.const.Typography
13
13
  import vn.momo.kits.modifier.setAutomationId
14
14
 
15
+ internal const val HEADER_FONT_FAMILY = "MoMoTrustDisplay"
16
+
15
17
  @Composable
16
18
  internal fun HeaderTitle(
17
19
  title: String = "",
@@ -27,6 +29,7 @@ internal fun HeaderTitle(
27
29
  fontSize = 15.sp,
28
30
  lineHeight = 22.sp,
29
31
  ),
32
+ fontFamily = HEADER_FONT_FAMILY,
30
33
  color = color,
31
34
  maxLines = 1,
32
35
  overflow = TextOverflow.Ellipsis
@@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip
16
16
  import androidx.compose.ui.draw.clipToBounds
17
17
  import androidx.compose.ui.layout.ContentScale
18
18
  import androidx.compose.ui.text.TextStyle
19
+ import androidx.compose.ui.text.font.FontWeight
19
20
  import androidx.compose.ui.text.style.TextOverflow
20
21
  import androidx.compose.ui.unit.Dp
21
22
  import androidx.compose.ui.unit.dp
@@ -28,6 +29,7 @@ import vn.momo.kits.const.AppTheme
28
29
  import vn.momo.kits.const.Colors
29
30
  import vn.momo.kits.const.Spacing
30
31
  import vn.momo.kits.const.Typography
32
+ import vn.momo.kits.const.getFontFamily
31
33
  import vn.momo.kits.modifier.activeOpacityClickable
32
34
  import vn.momo.kits.navigation.component.HeaderTitle.User
33
35
  import vn.momo.kits.platform.getScreenDimensions
@@ -77,6 +79,7 @@ internal fun HeaderUser(
77
79
  text = data.title,
78
80
  color = data.tintColor,
79
81
  style = Typography.actionXsBold,
82
+ fontFamily = HEADER_FONT_FAMILY,
80
83
  maxLines = 1,
81
84
  overflow = TextOverflow.Ellipsis
82
85
  )
@@ -303,7 +306,8 @@ private fun ManyAvatar(options: Options, modifier: Modifier = Modifier, urls: Av
303
306
  BasicText(
304
307
  text = "+$more",
305
308
  style = TextStyle(
306
- color = Colors.pink_03
309
+ color = Colors.pink_03,
310
+ fontFamily = getFontFamily(AppTheme.current.font, FontWeight.Normal)
307
311
  ),
308
312
  maxLines = 1,
309
313
  autoSize = TextAutoSize.StepBased(
@@ -3,13 +3,10 @@ package vn.momo.kits.platform
3
3
  import androidx.compose.runtime.Composable
4
4
  import androidx.compose.runtime.State
5
5
 
6
- /**
7
- * Progress of an in-flight back gesture as a fraction of the container width, or `null` while no
8
- * gesture is running. Observing only — it must never consume the gesture, otherwise it would steal
9
- * it from NavHost.
10
- *
11
- * Android always returns `null`: its NavHost runs on `androidx.activity`'s OnBackPressedDispatcher,
12
- * which exposes progress only to the top-most enabled callback, and that has to stay NavHost's.
13
- */
6
+ /** Fraction of container width, `null` when idle. Observe only — consuming would steal the
7
+ * gesture from NavHost. Always `null` on Android. */
14
8
  @Composable
15
9
  expect fun rememberBackGestureProgress(): State<Float?>
10
+
11
+ /** Android is a no-op: no host-owned recogniser to hand the gesture to. */
12
+ expect fun reportFloatingArrowBack(shouldUse: Boolean)
@@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
5
5
  import androidx.compose.ui.graphics.Color
6
6
  import androidx.compose.ui.graphics.NativePaint
7
7
  import androidx.compose.ui.unit.Dp
8
+ import vn.momo.kits.navigation.Navigator
8
9
 
9
10
  data class ScreenDimension(
10
11
  val width: Int,
@@ -24,8 +25,9 @@ expect fun getStatusBarHeight(): Dp
24
25
  @Composable
25
26
  expect fun BackHandler(enabled: Boolean, onBack: () -> Unit)
26
27
 
28
+ @Suppress("ParamsComparedByRef")
27
29
  @Composable
28
- expect fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit)
30
+ expect fun ProvideNavigationEventDispatcherOwner(navigator: Navigator, content: @Composable () -> Unit)
29
31
 
30
32
  @Composable
31
33
  expect fun getScreenHeight(): Dp
@@ -0,0 +1,45 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import androidx.compose.runtime.remember
6
+ import androidx.navigationevent.NavigationEvent
7
+ import androidx.navigationevent.NavigationEventDispatcher
8
+ import androidx.navigationevent.NavigationEventInput
9
+
10
+ @Suppress("ParamsComparedByRef")
11
+ @Composable
12
+ internal fun AttachHostBackGesture(dispatcher: NavigationEventDispatcher, navigator: Navigator) {
13
+ val input = remember(dispatcher) { HostNavigationEventInput() }
14
+ val target = remember(input, navigator) {
15
+ object : HostBackTarget {
16
+ override fun canPop() = navigator.canPop()
17
+ override fun onBack() = navigator.onBackSafe()
18
+ override fun backStarted() = input.backStarted()
19
+ override fun backProgressed(progress: Float) = input.backProgressed(progress)
20
+ override fun backCompleted() = input.backCompleted()
21
+ override fun backCancelled() = input.backCancelled()
22
+ }
23
+ }
24
+
25
+ DisposableEffect(dispatcher, target) {
26
+ dispatcher.addInput(input)
27
+ HostBackGestureBridge.attach(target)
28
+ onDispose {
29
+ HostBackGestureBridge.detach(target)
30
+ dispatcher.removeInput(input)
31
+ }
32
+ }
33
+ }
34
+
35
+ internal class HostNavigationEventInput : NavigationEventInput() {
36
+ fun backStarted() = dispatchOnBackStarted(NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT))
37
+
38
+ fun backProgressed(progress: Float) = dispatchOnBackProgressed(
39
+ NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT, progress = progress)
40
+ )
41
+
42
+ fun backCancelled() = dispatchOnBackCancelled()
43
+
44
+ fun backCompleted() = dispatchOnBackCompleted()
45
+ }
@@ -8,10 +8,9 @@ import androidx.compose.runtime.remember
8
8
  import androidx.compose.ui.InternalComposeUiApi
9
9
  import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
10
10
  import androidx.navigationevent.NavigationEventTransitionState
11
+ import vn.momo.kits.navigation.HostBackGestureBridge
11
12
 
12
- // NavigationEventDispatcher.transitionState is a passive observer: UIKitNavigationEventInput
13
- // publishes every edge-pan event to it without any handler consuming the gesture. Its `progress`
14
- // is touch.x / view width, i.e. a plain fraction of the screen width.
13
+ // transitionState is a passive observer; its progress is touch.x / view width.
15
14
  @OptIn(InternalComposeUiApi::class)
16
15
  @Composable
17
16
  actual fun rememberBackGestureProgress(): State<Float?> {
@@ -33,3 +32,7 @@ actual fun rememberBackGestureProgress(): State<Float?> {
33
32
 
34
33
  return progress
35
34
  }
35
+
36
+ actual fun reportFloatingArrowBack(shouldUse: Boolean) {
37
+ HostBackGestureBridge.shouldUseFloatingArrowBack = shouldUse
38
+ }
@@ -39,7 +39,8 @@ import platform.Foundation.NSBundle
39
39
  import platform.UIKit.UIColor
40
40
  import platform.UIKit.UIDevice
41
41
  import platform.UIKit.UIScreen
42
- import vn.momo.kits.navigation.ObserveBackGestureOwnership
42
+ import vn.momo.kits.navigation.AttachHostBackGesture
43
+ import vn.momo.kits.navigation.Navigator
43
44
 
44
45
  actual fun getPlatformName(): String = "iOS"
45
46
 
@@ -62,9 +63,6 @@ actual fun getStatusBarHeight(): Dp {
62
63
  return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
63
64
  }
64
65
 
65
- // An enabled kit BackHandler is registered inside the NavHost destination, so it out-ranks
66
- // NavHost's own predictive-back handler and swallows the gesture — that is how a screen opts out
67
- // of swipe-back (the port of RN's `hasCustomBackHandling`).
68
66
  @Suppress("DEPRECATION")
69
67
  @OptIn(ExperimentalComposeUiApi::class)
70
68
  @Composable
@@ -74,30 +72,17 @@ actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
74
72
 
75
73
  @OptIn(InternalComposeUiApi::class)
76
74
  @Composable
77
- actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
78
- val platformOwner = LocalCompatNavigationEventDispatcherOwner.current
79
- if (platformOwner != null) {
80
- // UIKitNavigationEventInput is registered on the scene's dispatcher, and that dispatcher
81
- // having enabled handlers is what arms Compose's UIKitBackGestureRecognizer. Providing a
82
- // fresh NavigationEventDispatcher() here would shadow it with an input-less one that never
83
- // sees a single edge-pan event — no recogniser, and no finger-following seek.
84
- ObserveBackGestureOwnership(platformOwner.navigationEventDispatcher)
85
- content()
86
- return
87
- }
88
-
89
- // Bare UIView hosts have no scene dispatcher, and BackHandler/PredictiveBackHandler error() on
90
- // a null owner. Disabled: nothing can feed it, so it exists only to keep those from throwing.
91
- val fallbackOwner = remember {
75
+ actual fun ProvideNavigationEventDispatcherOwner(navigator: Navigator, content: @Composable () -> Unit) {
76
+ val navigationEventDispatcherOwner = remember {
92
77
  object : NavigationEventDispatcherOwner {
93
- override val navigationEventDispatcher = NavigationEventDispatcher().apply {
94
- isEnabled = false
95
- }
78
+ override val navigationEventDispatcher = NavigationEventDispatcher()
96
79
  }
97
80
  }
98
81
 
82
+ AttachHostBackGesture(navigationEventDispatcherOwner.navigationEventDispatcher, navigator)
83
+
99
84
  CompositionLocalProvider(
100
- LocalCompatNavigationEventDispatcherOwner provides fallbackOwner,
85
+ LocalCompatNavigationEventDispatcherOwner provides navigationEventDispatcherOwner,
101
86
  content = content
102
87
  )
103
88
  }
@@ -1,5 +1,5 @@
1
1
  [versions]
2
- android-compileSdk = "36"
2
+ android-compileSdk = "37"
3
3
  android-minSdk = "24"
4
4
  android-targetSdk = "36"
5
5
  ktor-version = "3.0.0-rc-1"