@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.3-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 (48) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.local.json +185 -0
  3. package/CLAUDE.md +8 -3
  4. package/compose/build.gradle.kts +15 -1
  5. package/compose/build.gradle.kts.backup +14 -0
  6. package/compose/compose.podspec +1 -1
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +52 -25
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +45 -15
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +34 -8
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +81 -62
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +281 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +31 -4
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +27 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +17 -10
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  20. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  21. package/gradle.properties +1 -1
  22. package/ios/Application/HeaderRight.swift +25 -12
  23. package/ios/Application/Layout/ScreenLayout.swift +196 -0
  24. package/ios/Application/Navigation/BottomTab/BottomTab.swift +30 -9
  25. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
  26. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +275 -0
  27. package/ios/Application/Navigation/HeaderUser.swift +4 -1
  28. package/ios/Application/Navigation/NavigationContainer.swift +3 -0
  29. package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
  30. package/ios/Application/Navigation/Overplay/ModalScreen.swift +5 -1
  31. package/ios/Application/Navigation/component/DuoScrollGradient.swift +55 -0
  32. package/ios/Application/Navigation/component/Header.swift +9 -3
  33. package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
  34. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  35. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +61 -9
  36. package/ios/Application/Navigation/component/SideBar.swift +155 -0
  37. package/ios/Application/StackScreen.swift +135 -18
  38. package/ios/Popup/PopupDisplay.swift +84 -68
  39. package/ios/Popup/PopupLayout.swift +45 -0
  40. package/ios/Popup/PopupNotify.swift +43 -32
  41. package/ios/Popup/PopupPromotion.swift +17 -5
  42. package/ios/native-kits.podspec +1 -1
  43. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  44. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +76 -0
  45. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  46. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +170 -0
  47. package/local.properties +8 -0
  48. package/package.json +1 -1
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.offset
20
20
  import androidx.compose.foundation.layout.padding
21
21
  import androidx.compose.foundation.layout.size
22
22
  import androidx.compose.foundation.layout.width
23
+ import androidx.compose.foundation.layout.absolutePadding
23
24
  import androidx.compose.foundation.shape.RoundedCornerShape
24
25
  import androidx.compose.runtime.Composable
25
26
  import androidx.compose.runtime.DisposableEffect
@@ -51,6 +52,10 @@ import vn.momo.kits.modifier.conditional
51
52
  import vn.momo.kits.modifier.noFeedbackClickable
52
53
  import vn.momo.kits.platform.BackHandler
53
54
  import vn.momo.kits.platform.getScreenHeight
55
+ import vn.momo.kits.layout.getScreenLayout
56
+
57
+ // iPhone Duo inner display: center the sheet instead of spanning the width (RN BottomSheet).
58
+ private val DUO_REGULAR_MIN_WIDTH = 600.dp
54
59
 
55
60
  @Composable
56
61
  internal fun BottomSheet(
@@ -63,7 +68,11 @@ internal fun BottomSheet(
63
68
  footerComponent: (@Composable () -> Unit)? = null,
64
69
  onDismiss: (() -> Unit)?
65
70
  ) {
66
- val screenHeightDp = getScreenHeight()
71
+ val layout = getScreenLayout()
72
+ val isDuo = layout.isIPhoneDuoDevice
73
+ val isDuoCentered = isDuo && layout.width >= DUO_REGULAR_MIN_WIDTH
74
+ // the screen is the outer display on iPhone Duo, even when unfolded
75
+ val screenHeightDp = if (isDuo) layout.height else getScreenHeight()
67
76
  val screenHeightPx = with(LocalDensity.current) { screenHeightDp.toPx() }
68
77
  val sheetOffset = remember { Animatable(screenHeightPx) }
69
78
 
@@ -148,13 +157,32 @@ internal fun BottomSheet(
148
157
  }
149
158
  ) {
150
159
  val parentHeight = with(LocalDensity.current) { constraints.maxHeight.toDp() }
160
+ val maxHeight = if (isDuo) {
161
+ parentHeight - maxOf(layout.duoTopInset, Spacing.XL)
162
+ } else {
163
+ parentHeight - 90.dp
164
+ }
165
+ // landscape side buttons span the full height; in portrait the side status bar only covers the top
166
+ val sideInset = isDuo && !isDuoCentered && layout.width > layout.height
151
167
 
152
168
  Box(
153
169
  modifier = Modifier
154
170
  .align(Alignment.BottomCenter)
155
171
  .offset { IntOffset(x = 0, y = sheetOffset.value.toInt()) }
156
- .fillMaxWidth()
157
- .heightIn(max = parentHeight - 90.dp)
172
+ .then(
173
+ if (isDuoCentered) {
174
+ // keeps the portrait width in landscape, like system sheets
175
+ Modifier.width(
176
+ minOf(
177
+ minOf(layout.width, layout.height) - Spacing.L * 2,
178
+ layout.width - maxOf(layout.insetLeft, layout.insetRight, Spacing.L) * 2,
179
+ )
180
+ )
181
+ } else {
182
+ Modifier.fillMaxWidth()
183
+ }
184
+ )
185
+ .heightIn(max = maxHeight)
158
186
  .background(
159
187
  color = if (isSurface) AppTheme.current.colors.background.surface else AppTheme.current.colors.background.default,
160
188
  shape = RoundedCornerShape(topStart = Radius.M, topEnd = Radius.M)
@@ -168,7 +196,13 @@ internal fun BottomSheet(
168
196
  val bottomInset = if (useBottomInset) {
169
197
  (AppNavigationBar.current - keyboardSizeState().value).coerceAtLeast(0.dp)
170
198
  } else 0.dp
171
- Column(Modifier.padding(bottom = bottomInset)) {
199
+ Column(
200
+ Modifier
201
+ .padding(bottom = bottomInset)
202
+ .conditional(sideInset) {
203
+ absolutePadding(left = layout.insetLeft, right = layout.insetRight)
204
+ }
205
+ ) {
172
206
  Box(
173
207
  modifier = Modifier
174
208
  .height(72.dp)
@@ -249,6 +283,11 @@ internal fun BottomSheet(
249
283
  content()
250
284
  }
251
285
  BottomSheetFooter(footerComponent)
286
+ } else if (isDuo) {
287
+ // content shrinks so a tall sheet keeps its header
288
+ Box(Modifier.weight(1f, fill = false)) {
289
+ content()
290
+ }
252
291
  } else {
253
292
  content()
254
293
  }
@@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
9
9
  import androidx.compose.foundation.interaction.MutableInteractionSource
10
10
  import androidx.compose.foundation.layout.Box
11
11
  import androidx.compose.foundation.layout.fillMaxSize
12
+ import androidx.compose.foundation.layout.padding
12
13
  import androidx.compose.foundation.layout.wrapContentSize
13
14
  import androidx.compose.runtime.*
14
15
  import androidx.compose.ui.Alignment
@@ -25,6 +26,7 @@ import vn.momo.kits.modifier.conditional
25
26
  import vn.momo.kits.modifier.noFeedbackClickable
26
27
  import vn.momo.kits.navigation.tracking.ScreenTracker
27
28
  import vn.momo.kits.platform.BackHandler
29
+ import vn.momo.kits.layout.getScreenLayout
28
30
 
29
31
  @Composable
30
32
  internal fun ModalScreen(
@@ -100,12 +102,18 @@ internal fun ModalScreen(
100
102
  closeEvent()
101
103
  }
102
104
 
105
+ val layout = getScreenLayout()
106
+
103
107
  Box(
104
108
  modifier = Modifier
105
109
  .fillMaxSize()
106
110
  .background(Color.Black.copy(alpha = 0.6f * alpha.value))
107
111
  .noFeedbackClickable {
108
112
  if(barrierDismissible) closeEvent()
113
+ }
114
+ // center below the side status bar and above the home indicator
115
+ .conditional(layout.isIPhoneDuoDevice) {
116
+ padding(top = layout.duoTopInset, bottom = layout.insetBottom)
109
117
  },
110
118
  contentAlignment = Alignment.Center
111
119
  ) {
@@ -65,6 +65,8 @@ import vn.momo.kits.navigation.component.Header
65
65
  import vn.momo.kits.navigation.component.HeaderBackground
66
66
  import vn.momo.kits.navigation.component.HeaderRight
67
67
  import vn.momo.kits.navigation.component.HeaderSideBar
68
+ import vn.momo.kits.navigation.bottomtab.LocalDuoRailHeight
69
+ import vn.momo.kits.navigation.bottomtab.duoFloatingTabBarInset
68
70
  import vn.momo.kits.navigation.component.HeaderType
69
71
  import vn.momo.kits.navigation.component.InputSearchType
70
72
  import vn.momo.kits.platform.BackHandler
@@ -75,6 +77,7 @@ import kotlinx.coroutines.delay
75
77
  import vn.momo.kits.application.LocalContext
76
78
  import vn.momo.kits.utils.horizontalSafeAreaPadding
77
79
  import vn.momo.kits.layout.LocalScreenLayout
80
+ import vn.momo.kits.layout.ScreenLayout
78
81
  import vn.momo.kits.layout.consumeHorizontalInsets
79
82
  import vn.momo.kits.layout.getScreenLayout
80
83
  import kotlin.time.Clock
@@ -84,11 +87,19 @@ import kotlin.time.ExperimentalTime
84
87
  internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
85
88
  internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
86
89
 
90
+ // Space a tab's content leaves for the tab bar and home indicator. On iPhone Duo the floating bar
91
+ // overlays the content and the rail lives in the side strip, so only their own inset applies.
87
92
  @Composable
88
- private fun bottomTabBarInset(): Dp {
89
- if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
93
+ private fun bottomTabChildInset(): Dp {
90
94
  val keyboard = keyboardSizeState().value
91
- return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
95
+ val navigationBar = AppNavigationBar.current
96
+ val layout = getScreenLayout()
97
+ return when {
98
+ layout.isIPhoneDuo && layout.sideBar == null -> (duoFloatingTabBarInset(layout) - keyboard).coerceAtLeast(0.dp)
99
+ layout.isIPhoneDuo -> (navigationBar - keyboard).coerceAtLeast(0.dp)
100
+ else -> (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp) +
101
+ (navigationBar - keyboard).coerceAtLeast(0.dp)
102
+ }
92
103
  }
93
104
 
94
105
  @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
@@ -212,7 +223,10 @@ internal fun StackScreen(
212
223
  Header(onBackHandler)
213
224
  }
214
225
 
215
- HeaderSideBar(bottomInset = bottomBarsHeight(isBottomTabChild), modifier = Modifier.zIndex(4f))
226
+ HeaderSideBar(
227
+ bottomInset = maxOf(bottomBarsHeight(isBottomTabChild), if (isBottomTabChild) LocalDuoRailHeight.current else 0.dp),
228
+ modifier = Modifier.zIndex(4f),
229
+ )
216
230
 
217
231
  Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
218
232
  SearchAnimated(snapState = snapState)
@@ -239,7 +253,7 @@ private fun bottomBarsHeight(isBottomTabChild: Boolean): Dp {
239
253
  val navigationBar = AppNavigationBar.current
240
254
  return when {
241
255
  options.hasFooter() -> with(LocalDensity.current) { LocalFooterHeightPx.current.intValue.toDp() }
242
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
256
+ isBottomTabChild -> bottomTabChildInset()
243
257
  else -> navigationBar
244
258
  }
245
259
  }
@@ -274,10 +288,11 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
274
288
  val scrollState = LocalScrollState.current
275
289
  val navigationBar = AppNavigationBar.current
276
290
  val screenLayout = getScreenLayout()
291
+ val useDuoGlass = options.usesDuoGlassHeader(screenLayout)
277
292
 
278
293
  Spacer(
279
294
  Modifier.height(
280
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
295
+ if (!useDuoGlass && (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent)))
281
296
  screenLayout.headerHeight else 0.dp
282
297
  )
283
298
  )
@@ -297,10 +312,13 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
297
312
  .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
298
313
  verticalScroll(scrollState)
299
314
  }
315
+ .conditional(useDuoGlass) {
316
+ padding(top = screenLayout.headerHeight)
317
+ }
300
318
  .padding(
301
319
  bottom = when {
302
320
  options.hasFooter() || isBottomTabRoot -> 0.dp
303
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
321
+ isBottomTabChild -> bottomTabChildInset()
304
322
  !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
305
323
  else -> navigationBar
306
324
  }
@@ -343,7 +361,9 @@ internal fun FooterContent() {
343
361
  val options = LocalOptions.current
344
362
  if (options.hasFooter()) {
345
363
  val keyboardSize = keyboardSizeState()
346
- val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
364
+ // the Duo floating tab bar overlays tab content, so a tab's footer sits above it
365
+ val duoTabInset = if (LocalNavigation.current.bottomTabIndex != -1) duoFloatingTabBarInset(getScreenLayout()) else 0.dp
366
+ val bottomPadding = (maxOf(AppNavigationBar.current, duoTabInset) - keyboardSize.value).coerceAtLeast(0.dp)
347
367
  Footer(
348
368
  footerComponent = options.footerComponent,
349
369
  bottomPadding = bottomPadding,
@@ -608,6 +628,12 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
608
628
 
609
629
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
610
630
 
631
+ // iPhone Duo side bar: content scrolls under a glass header instead of starting below it (RN Screen).
632
+ internal fun NavigationOptions.usesDuoGlassHeader(layout: ScreenLayout): Boolean =
633
+ layout.sideBar != null && headerType is HeaderType.DefaultOrExtended &&
634
+ getInputSearchType(this) == InputSearchType.None &&
635
+ scrollData.scrollable && scrollData.scrollState is ScrollState
636
+
611
637
  internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
612
638
  return when (val headerType = options.headerType) {
613
639
  is HeaderType.DefaultOrExtended -> when {
@@ -8,6 +8,7 @@ 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.CompositionLocalProvider
11
12
  import androidx.compose.runtime.DisposableEffect
12
13
  import androidx.compose.runtime.LaunchedEffect
13
14
  import androidx.compose.ui.Alignment
@@ -27,6 +28,7 @@ import vn.momo.kits.navigation.NavigationOptions
27
28
  import vn.momo.kits.navigation.StackScreen
28
29
  import vn.momo.kits.navigation.component.HeaderType
29
30
  import vn.momo.kits.platform.reportFloatingArrowBack
31
+ import vn.momo.kits.layout.getScreenLayout
30
32
 
31
33
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
32
34
  internal fun setBottomTabOption(index: Int, options: NavigationOptions){
@@ -78,81 +80,98 @@ fun BottomTab(
78
80
  }
79
81
  }
80
82
 
83
+ val layout = getScreenLayout()
84
+ val railHeight = if (layout.isIPhoneDuo && layout.sideBar != null) {
85
+ duoRailHeight(items.size, hasQr = floatingButton != null)
86
+ } else 0.dp
87
+ val onTabSelected: (Int) -> Unit = {
88
+ val targetRoute = "option$it"
89
+ if (currentRoute != targetRoute) {
90
+ navController.navigate(targetRoute)
91
+ }
92
+ }
93
+
81
94
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
82
95
  Box(modifier = Modifier
83
96
  .fillMaxSize()
84
97
  ) {
85
- NavHost(
86
- navController = navController,
87
- startDestination = "option0"
88
- ) {
89
- items.forEachIndexed { index, item ->
90
- composable(
91
- route = "option$index",
92
- enterTransition = {
93
- fadeIn(animationSpec = tween(200)) +
94
- scaleIn(
95
- initialScale = 0.97f,
96
- animationSpec = tween(200)
97
- )
98
- },
99
- exitTransition = {
100
- fadeOut(animationSpec = tween(200)) +
101
- scaleOut(
102
- targetScale = 0.97f,
103
- animationSpec = tween(200)
104
- )
105
- },
106
- popEnterTransition = {
107
- fadeIn(animationSpec = tween(200)) +
108
- scaleIn(
109
- initialScale = 0.97f,
110
- animationSpec = tween(200)
111
- )
112
- },
113
- popExitTransition = {
114
- fadeOut(animationSpec = tween(200)) +
115
- scaleOut(
116
- targetScale = 0.97f,
117
- animationSpec = tween(200)
118
- )
119
- }
120
- ) {
121
- val option = getBottomTabOption(index)
98
+ CompositionLocalProvider(LocalDuoRailHeight provides railHeight) {
99
+ NavHost(
100
+ navController = navController,
101
+ startDestination = "option0"
102
+ ) {
103
+ items.forEachIndexed { index, item ->
104
+ composable(
105
+ route = "option$index",
106
+ enterTransition = {
107
+ fadeIn(animationSpec = tween(200)) +
108
+ scaleIn(
109
+ initialScale = 0.97f,
110
+ animationSpec = tween(200)
111
+ )
112
+ },
113
+ exitTransition = {
114
+ fadeOut(animationSpec = tween(200)) +
115
+ scaleOut(
116
+ targetScale = 0.97f,
117
+ animationSpec = tween(200)
118
+ )
119
+ },
120
+ popEnterTransition = {
121
+ fadeIn(animationSpec = tween(200)) +
122
+ scaleIn(
123
+ initialScale = 0.97f,
124
+ animationSpec = tween(200)
125
+ )
126
+ },
127
+ popExitTransition = {
128
+ fadeOut(animationSpec = tween(200)) +
129
+ scaleOut(
130
+ targetScale = 0.97f,
131
+ animationSpec = tween(200)
132
+ )
133
+ }
134
+ ) {
135
+ val option = getBottomTabOption(index)
122
136
 
123
- StackScreen(
124
- content = item.screen,
125
- navigationOptions = option,
126
- bottomTabIndex = index,
127
- onBackHandler = {
128
- if (index != 0) {
129
- navController.popBackStack(
130
- navController.graph.startDestinationRoute!!,
131
- inclusive = false
132
- )
133
- } else {
134
- navigator.pop()
137
+ StackScreen(
138
+ content = item.screen,
139
+ navigationOptions = option,
140
+ bottomTabIndex = index,
141
+ onBackHandler = {
142
+ if (index != 0) {
143
+ navController.popBackStack(
144
+ navController.graph.startDestinationRoute!!,
145
+ inclusive = false
146
+ )
147
+ } else {
148
+ navigator.pop()
149
+ }
135
150
  }
136
- }
137
- )
151
+ )
152
+ }
138
153
  }
139
154
  }
140
155
  }
141
156
  }
142
- Column {
143
- BottomTabBar(
157
+ if (layout.isIPhoneDuo) {
158
+ DuoTabBar(
159
+ layout = layout,
144
160
  items = items,
145
161
  floatingButton = floatingButton,
146
- navController = navController,
147
- onTabSelected = {
148
- val currentRoute = navController.currentBackStackEntry?.destination?.route
149
- val targetRoute = "option$it"
150
- if (currentRoute != targetRoute) {
151
- navController.navigate(targetRoute)
152
- }
153
- }
162
+ selectedIndex = items.indices.firstOrNull { "option$it" == currentRoute } ?: 0,
163
+ onTabSelected = onTabSelected,
154
164
  )
155
- Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
165
+ } else {
166
+ Column {
167
+ BottomTabBar(
168
+ items = items,
169
+ floatingButton = floatingButton,
170
+ navController = navController,
171
+ onTabSelected = onTabSelected,
172
+ )
173
+ Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
174
+ }
156
175
  }
157
176
  }
158
177
  }