@momo-kits/native-kits 0.164.1-trustsans-12-debug → 0.165.1-beta.10-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 (31) hide show
  1. package/.codegraph/codegraph.db +0 -0
  2. package/.codegraph/daemon.log +100 -0
  3. package/CLAUDE.md +1 -0
  4. package/compose/build.gradle.kts +1 -1
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +18 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +30 -4
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +104 -22
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -9
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +8 -4
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +2 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +66 -31
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +22 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +25 -5
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +19 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +13 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  24. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +98 -0
  25. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  26. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +13 -3
  27. package/gradle.properties +1 -1
  28. package/ios/native-kits.podspec +1 -1
  29. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  30. package/package.json +1 -1
  31. package/.codegraph/.gitignore +0 -5
Binary file
@@ -0,0 +1,100 @@
1
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 51873, v1.1.0). Idle timeout 300000ms.
2
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
3
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
4
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 58673, v1.1.0). Idle timeout 300000ms.
5
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
6
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
7
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 80799, v1.1.0). Idle timeout 300000ms.
8
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
9
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
10
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 34253, v1.1.0). Idle timeout 300000ms.
11
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
12
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
13
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 65894, v1.1.0). Idle timeout 300000ms.
14
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
15
+ [CodeGraph MCP] Auto-synced 1 file(s) in 446ms
16
+ [CodeGraph MCP] Auto-synced 1 file(s) in 219ms
17
+ [CodeGraph MCP] Auto-synced 1 file(s) in 212ms
18
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
19
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 68727, v1.1.0). Idle timeout 300000ms.
20
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
21
+ [CodeGraph MCP] Auto-synced 1 file(s) in 466ms
22
+ [CodeGraph MCP] Auto-synced 1 file(s) in 225ms
23
+ [CodeGraph MCP] Auto-synced 1 file(s) in 218ms
24
+ [CodeGraph MCP] Auto-synced 1 file(s) in 211ms
25
+ [CodeGraph MCP] Auto-synced 1 file(s) in 203ms
26
+ [CodeGraph MCP] Auto-synced 1 file(s) in 200ms
27
+ [CodeGraph MCP] Auto-synced 1 file(s) in 204ms
28
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
29
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 97060, v1.1.0). Idle timeout 300000ms.
30
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
31
+ [CodeGraph MCP] Caught up 1 file(s) changed since last run
32
+ [CodeGraph MCP] Auto-synced 1 file(s) in 485ms
33
+ [CodeGraph MCP] Auto-synced 1 file(s) in 328ms
34
+ [CodeGraph MCP] Auto-synced 1 file(s) in 280ms
35
+ [CodeGraph MCP] Auto-synced 1 file(s) in 208ms
36
+ [CodeGraph MCP] Auto-synced 1 file(s) in 206ms
37
+ [CodeGraph MCP] Auto-synced 1 file(s) in 178ms
38
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
39
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 2726, v1.1.0). Idle timeout 300000ms.
40
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
41
+ [CodeGraph MCP] Auto-synced 1 file(s) in 427ms
42
+ [CodeGraph MCP] Auto-synced 1 file(s) in 178ms
43
+ [CodeGraph MCP] Auto-synced 1 file(s) in 170ms
44
+ [CodeGraph MCP] Auto-synced 1 file(s) in 196ms
45
+ [CodeGraph MCP] Auto-synced 1 file(s) in 180ms
46
+ [CodeGraph MCP] Auto-synced 1 file(s) in 146ms
47
+ [CodeGraph MCP] Auto-synced 1 file(s) in 170ms
48
+ [CodeGraph MCP] Auto-synced 1 file(s) in 190ms
49
+ [CodeGraph MCP] Auto-synced 1 file(s) in 178ms
50
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
51
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 8036, v1.1.0). Idle timeout 300000ms.
52
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
53
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
54
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 14133, v1.1.0). Idle timeout 300000ms.
55
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
56
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
57
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 34560, v1.1.0). Idle timeout 300000ms.
58
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
59
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
60
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 73920, v1.1.0). Idle timeout 300000ms.
61
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
62
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
63
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 8855, v1.1.0). Idle timeout 300000ms.
64
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
65
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
66
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 39954, v1.1.0). Idle timeout 300000ms.
67
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
68
+ [CodeGraph MCP] Auto-synced 124 file(s) in 1892ms
69
+ [CodeGraph MCP] Auto-synced 1 file(s) in 247ms
70
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
71
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 70751, v1.1.0). Idle timeout 300000ms.
72
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
73
+ [CodeGraph MCP] Caught up 1 file(s) changed since last run
74
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
75
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 89552, v1.1.0). Idle timeout 300000ms.
76
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
77
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
78
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 98891, v1.1.0). Idle timeout 300000ms.
79
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
80
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
81
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 57037, v1.1.0). Idle timeout 300000ms.
82
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
83
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
84
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 32794, v1.1.0). Idle timeout 300000ms.
85
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
86
+ [CodeGraph MCP] Caught up 4 file(s) changed since last run
87
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
88
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 57725, v1.1.0). Idle timeout 300000ms.
89
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
90
+ [CodeGraph MCP] Caught up 17 file(s) changed since last run
91
+ [CodeGraph MCP] Auto-synced 13 file(s) in 241ms
92
+ [CodeGraph MCP] Auto-synced 13 file(s) in 246ms
93
+ [CodeGraph MCP] Auto-synced 1 file(s) in 181ms
94
+ [CodeGraph MCP] Auto-synced 1 file(s) in 186ms
95
+ [CodeGraph MCP] Auto-synced 1 file(s) in 176ms
96
+ [CodeGraph daemon] Shutting down (idle timeout; clients=0).
97
+ [CodeGraph daemon] Listening on /Users/sonnguyen/Documents/Momo/momo-native-kits/.codegraph/daemon.sock (pid 26309, v1.1.0). Idle timeout 300000ms.
98
+ [CodeGraph MCP] File watcher active — graph will auto-sync on changes
99
+ [CodeGraph MCP] Caught up 267 file(s) changed since last run
100
+ [CodeGraph daemon] Shutting down (inactivity backstop; clients=1).
package/CLAUDE.md CHANGED
@@ -41,6 +41,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
41
41
 
42
42
  - **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. **Do not use** `ApplicationContainer` (`application/NavigationContainer.kt`) — deprecated and does **not** wire `LocalLocalize` / `LocalNavigator` / `LocalMaxApi`.
43
43
  - **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
44
+ - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
44
45
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
45
46
  - **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
46
47
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.164.1-trustsans-12-debug"
43
+ version = "0.165.1-beta.10-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.164.1-trustsans-9-debug'
3
+ spec.version = '0.165.1-beta.10-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -0,0 +1,12 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.State
5
+ import androidx.compose.runtime.mutableStateOf
6
+ import androidx.compose.runtime.remember
7
+
8
+ // No passive observer at androidx.activity 1.10.x; a second callback would steal the gesture.
9
+ @Composable
10
+ actual fun rememberBackGestureProgress(): State<Float?> = remember { mutableStateOf(null) }
11
+
12
+ actual fun reportFloatingArrowBack(shouldUse: Boolean) = Unit
@@ -1,6 +1,9 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.runtime.Immutable
4
+ import androidx.compose.runtime.getValue
5
+ import androidx.compose.runtime.mutableStateOf
6
+ import androidx.compose.runtime.setValue
4
7
  import androidx.compose.runtime.staticCompositionLocalOf
5
8
  import vn.momo.kits.components.TrustBannerData
6
9
 
@@ -16,6 +19,17 @@ data class DesignSystemConfig(
16
19
  val headerGradient: String? = null,
17
20
  )
18
21
 
22
+ // App-wide holder seeded by the host (same pattern as KitFontScale), so direct NavigationContainer
23
+ // callers get the config without passing it. Snapshot state: a config fetched after first frame recomposes.
24
+ object KitDesignSystemConfig {
25
+ var config by mutableStateOf<DesignSystemConfig?>(null)
26
+ private set
27
+
28
+ fun update(config: DesignSystemConfig?) {
29
+ this.config = config
30
+ }
31
+ }
32
+
19
33
  @Immutable
20
34
  data class MiniAppContext(
21
35
  val appIcon: String = "",
@@ -102,5 +116,9 @@ val LocalComponentInformation = staticCompositionLocalOf<ComponentInformation?>
102
116
  null
103
117
  }
104
118
 
119
+ val LocalDesignSystemConfig = staticCompositionLocalOf<DesignSystemConfig?> {
120
+ null
121
+ }
122
+
105
123
  @Deprecated("Max font scale is fixed at MAX_FONT_SCALE (1.5); this CompositionLocal is no longer read.")
106
124
  val ScaleSizeMaxRate = staticCompositionLocalOf<Float?> { null }
@@ -1,5 +1,6 @@
1
1
  package vn.momo.kits.components
2
2
 
3
+ import androidx.compose.foundation.Canvas
3
4
  import androidx.compose.foundation.background
4
5
  import androidx.compose.foundation.layout.Box
5
6
  import androidx.compose.foundation.layout.fillMaxWidth
@@ -7,17 +8,42 @@ import androidx.compose.foundation.layout.height
7
8
  import androidx.compose.foundation.layout.padding
8
9
  import androidx.compose.runtime.Composable
9
10
  import androidx.compose.ui.Modifier
11
+ import androidx.compose.ui.geometry.Offset
12
+ import androidx.compose.ui.graphics.PathEffect
13
+ import androidx.compose.ui.graphics.StrokeCap
10
14
  import androidx.compose.ui.unit.dp
11
15
  import vn.momo.kits.const.AppTheme
12
16
  import vn.momo.kits.const.Spacing
13
17
 
18
+ enum class DividerType { DEFAULT, DASH }
19
+
14
20
  @Composable
15
- fun Divider() {
21
+ fun Divider(useMargin: Boolean = true, type: DividerType = DividerType.DEFAULT) {
22
+ val color = AppTheme.current.colors.border.default
23
+ val margin = if (useMargin) Spacing.XS else 0.dp
24
+ if (type == DividerType.DASH) {
25
+ Canvas(
26
+ modifier = Modifier
27
+ .fillMaxWidth()
28
+ .padding(vertical = margin)
29
+ .height(1.dp),
30
+ ) {
31
+ drawLine(
32
+ color = color,
33
+ start = Offset(0f, size.height / 2f),
34
+ end = Offset(size.width, size.height / 2f),
35
+ strokeWidth = size.height,
36
+ cap = StrokeCap.Round,
37
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx())),
38
+ )
39
+ }
40
+ return
41
+ }
16
42
  Box(
17
43
  modifier = Modifier
18
44
  .fillMaxWidth()
45
+ .padding(vertical = margin)
19
46
  .height(1.dp)
20
- .background(color = AppTheme.current.colors.border.default)
21
- .padding(vertical = Spacing.XS)
47
+ .background(color = color),
22
48
  )
23
- }
49
+ }
@@ -11,6 +11,9 @@ import androidx.compose.runtime.*
11
11
  import androidx.compose.ui.Alignment
12
12
  import androidx.compose.ui.Modifier
13
13
  import androidx.compose.ui.graphics.Color
14
+ import androidx.compose.ui.graphics.drawscope.withTransform
15
+ import androidx.compose.ui.graphics.drawscope.DrawScope
16
+ import androidx.compose.ui.graphics.Path
14
17
  import androidx.compose.ui.layout.onGloballyPositioned
15
18
  import androidx.compose.ui.layout.positionInParent
16
19
  import androidx.compose.ui.platform.LocalDensity
@@ -304,47 +307,126 @@ private fun CardTabBar(
304
307
  onPressTabItem: (Int) -> Unit,
305
308
  ) {
306
309
  val theme = AppTheme.current
307
-
308
- Row(
310
+ val surface = theme.colors.background.surface
311
+
312
+ // react-kits `CardTabBar` draws the bar in TWO layers: a flat strip where every
313
+ // tab looks unselected, and one raised card laid OVER it for the active tab.
314
+ // The card is 4dp taller, sits `Spacing.XS` higher, and is joined to the strip
315
+ // on each side by a curved "ear" — the browser-tab silhouette that gives this
316
+ // variant its name. Rendering the active tab in place (just taller) loses both
317
+ // the overlap and the ears, and the strip used the theme's `tonal` pink where
318
+ // react-kits uses `Colors.black_02`, so the bar came out pink and flat.
319
+ val pathWidth = if (tabs.size > 2) 22.dp else 28.dp
320
+ val isFirst = selectedIndex == 0
321
+ val isLast = selectedIndex == tabs.lastIndex
322
+
323
+ // The bar is exactly as tall as the FLAT strip — react-kits' raised card is
324
+ // `position: absolute`, so it never adds height; it just overflows upward into
325
+ // the 4dp margin. Letting the 44dp card size this box instead left a 4dp band of
326
+ // background showing between the tabs and the panel they sit on.
327
+ BoxWithConstraints(
309
328
  modifier = Modifier
310
329
  .fillMaxWidth()
311
- .padding(top = Spacing.XS),
312
- verticalAlignment = Alignment.Bottom,
330
+ .padding(top = Spacing.XS)
331
+ .height(CARD_TAB_HEIGHT),
313
332
  ) {
314
- tabs.forEachIndexed { index, tab ->
315
- val isActive = selectedIndex == index
316
- val iconColor = if (isActive) selectedColor else unselectedColor
333
+ val itemWidth = maxWidth / tabs.size
334
+
335
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) {
336
+ tabs.forEachIndexed { index, tab ->
337
+ Box(
338
+ modifier = Modifier
339
+ .width(itemWidth)
340
+ .height(CARD_TAB_HEIGHT)
341
+ .background(
342
+ color = Colors.black_02,
343
+ shape = RoundedCornerShape(topStart = Radius.M, topEnd = Radius.M),
344
+ )
345
+ .activeOpacityClickable { onPressTabItem(index) }
346
+ .padding(horizontal = Spacing.S),
347
+ contentAlignment = Alignment.Center,
348
+ ) {
349
+ Row(verticalAlignment = Alignment.CenterVertically) {
350
+ TabLeadingIcon(tab = tab, active = false, color = unselectedColor)
351
+ Text(
352
+ text = tab.title,
353
+ style = Typography.bodyDefaultRegular,
354
+ color = unselectedColor,
355
+ maxLines = 1,
356
+ overflow = TextOverflow.Ellipsis,
357
+ modifier = Modifier.weight(1f, fill = false),
358
+ )
359
+ TabTrailingBadge(tab)
360
+ }
361
+ }
362
+ }
363
+ }
317
364
 
365
+ val active = tabs[selectedIndex]
366
+ Row(
367
+ modifier = Modifier
368
+ // Pinned to the strip's BOTTOM edge so the two always end on the same
369
+ // pixel; `requiredHeight` then grows it upward into the 4dp margin.
370
+ // Offsetting it up by hand instead left a sub-dp sliver of strip
371
+ // showing under the card once rounding got involved.
372
+ .align(Alignment.BottomStart)
373
+ .offset(x = itemWidth * selectedIndex - (if (isFirst) 0.dp else pathWidth))
374
+ // `requiredHeight`, not `height`: the bar box is only as tall as the
375
+ // flat strip, so a plain height would be coerced down to 40 and the
376
+ // raised card would stop 4dp short of the panel below, leaving a grey
377
+ // sliver. react-kits gets this free — its card is absolutely
378
+ // positioned and ignores the parent box entirely.
379
+ .requiredHeight(CARD_TAB_ACTIVE_HEIGHT),
380
+ verticalAlignment = Alignment.Bottom,
381
+ ) {
382
+ // react-kits nudges each ear 8pt UNDER the card so the join has no seam.
383
+ if (!isFirst) CardTabEar(pathWidth, mirrored = true, color = surface, nudge = 8.dp)
318
384
  Box(
319
385
  modifier = Modifier
320
- .weight(1f)
321
- .height(if (isActive) 44.dp else 40.dp)
386
+ .width(itemWidth)
387
+ .fillMaxHeight()
322
388
  .background(
323
- color = if (isActive) theme.colors.background.surface
324
- else theme.colors.background.tonal,
325
- shape = RoundedCornerShape(
326
- topStart = Radius.M,
327
- topEnd = Radius.M,
328
- ),
389
+ color = surface,
390
+ shape = RoundedCornerShape(topStart = Radius.M, topEnd = Radius.M),
329
391
  )
330
- .activeOpacityClickable { onPressTabItem(index) }
331
- .padding(horizontal = Spacing.S),
392
+ .activeOpacityClickable { onPressTabItem(selectedIndex) }
393
+ .padding(horizontal = if (tabs.size == 3) Spacing.S else Spacing.M),
332
394
  contentAlignment = Alignment.Center,
333
395
  ) {
334
396
  Row(verticalAlignment = Alignment.CenterVertically) {
335
- TabLeadingIcon(tab = tab, active = isActive, color = iconColor)
397
+ TabLeadingIcon(tab = active, active = true, color = selectedColor)
336
398
  Text(
337
- text = tab.title,
338
- style = if (isActive) Typography.headerSSemibold else Typography.bodyDefaultRegular,
339
- color = if (isActive) selectedColor else unselectedColor,
399
+ text = active.title,
400
+ style = Typography.headerSSemibold,
401
+ color = selectedColor,
340
402
  maxLines = 1,
341
403
  overflow = TextOverflow.Ellipsis,
342
404
  modifier = Modifier.weight(1f, fill = false),
343
405
  )
344
- TabTrailingBadge(tab)
406
+ TabTrailingBadge(active)
345
407
  }
346
408
  }
409
+ if (!isLast) CardTabEar(pathWidth, mirrored = false, color = surface, nudge = -8.dp)
410
+ }
411
+ }
412
+ }
413
+
414
+ private val CARD_TAB_HEIGHT = 40.dp
415
+ private val CARD_TAB_ACTIVE_HEIGHT = 44.dp
416
+ @Composable
417
+ private fun CardTabEar(width: Dp, mirrored: Boolean, color: Color, nudge: Dp) {
418
+ Canvas(modifier = Modifier.offset(x = nudge).width(width).height(CARD_TAB_ACTIVE_HEIGHT)) {
419
+ val path = Path().apply {
420
+ moveTo(0f, 0f)
421
+ lineTo(0.60765f, 0f)
422
+ cubicTo(6.47582f, 0f, 11.4819f, 4.23831f, 12.4602f, 10.0244f)
423
+ cubicTo(13.9248f, 18.6872f, 15.9294f, 30.37f, 16.6925f, 34f)
424
+ cubicTo(17.9536f, 40f, 22.259f, 44f, 27.9996f, 44f)
425
+ lineTo(0f, 44f)
426
+ close()
347
427
  }
428
+ val draw: DrawScope.() -> Unit = { drawPath(path = path, color = color) }
429
+ if (mirrored) withTransform({ scale(-1f, 1f) }) { draw() } else draw()
348
430
  }
349
431
  }
350
432
 
@@ -78,15 +78,9 @@ fun Text(
78
78
  // Cache theme access to avoid repeated lookups
79
79
  val theme = AppTheme.current
80
80
 
81
- // Call @Composable functions directly in composable context
82
- val scaledFontSize = scaleSize(style.fontSize)
83
- val scaledLineHeight = scaleSize(style.lineHeight)
84
- val fontFamilyResult = getFontFamily(fontFamily ?: theme.font, style.fontWeight)
85
-
86
- // Now memoize the results
87
- val fontSize = remember(scaledFontSize) { scaledFontSize }
88
- val lineHeight = remember(scaledLineHeight) { scaledLineHeight }
89
- val font = remember(fontFamilyResult) { fontFamilyResult }
81
+ val fontSize = scaleSize(style.fontSize)
82
+ val lineHeight = scaleSize(style.lineHeight)
83
+ val font = getFontFamily(fontFamily ?: theme.font, style.fontWeight)
90
84
 
91
85
  // Memoize color calculation
92
86
  val textColor = remember(color, theme) {
@@ -47,9 +47,9 @@ fun Title(
47
47
  val theme = AppTheme.current
48
48
  val textStyle: TextStyle = when (type) {
49
49
  TitleType.Card -> when (size) {
50
- TitleSize.Small -> TextStyle(fontSize = 14.sp, lineHeight = 20.sp)
51
- TitleSize.Medium -> TextStyle(fontSize = 16.sp, lineHeight = 22.sp)
52
- TitleSize.Large -> TextStyle(fontSize = 18.sp, lineHeight = 26.sp)
50
+ TitleSize.Small -> TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Bold)
51
+ TitleSize.Medium -> TextStyle(fontSize = 16.sp, lineHeight = 22.sp, fontWeight = FontWeight.Bold)
52
+ TitleSize.Large -> TextStyle(fontSize = 18.sp, lineHeight = 26.sp, fontWeight = FontWeight.Bold)
53
53
  }
54
54
 
55
55
  TitleType.Section -> when (size) {
@@ -89,7 +89,11 @@ fun Title(
89
89
  .conditional(IsShowBaseLineDebug) {
90
90
  border(1.dp, Colors.blue_03)
91
91
  }
92
- .padding(8.dp),
92
+ // react-kits' Title root is `styles.wrapper` = `{flexDirection:'row'}`
93
+ // plus, for a SECTION only, `marginTop: Spacing.S` (foundations/Title
94
+ // index.tsx). It has no padding at all, so the 8dp box here inset every
95
+ // title by 8dp against the web render.
96
+ .padding(top = if (type == TitleType.Section) Spacing.S else 0.dp),
93
97
  verticalAlignment = Alignment.CenterVertically
94
98
  ) {
95
99
  icon?.let {
@@ -18,6 +18,7 @@ import androidx.compose.ui.draw.clip
18
18
  import androidx.compose.ui.graphics.Color
19
19
  import androidx.compose.ui.layout.ContentScale
20
20
  import androidx.compose.ui.unit.dp
21
+ import vn.momo.kits.application.LocalDesignSystemConfig
21
22
  import vn.momo.kits.application.LocalLocalize
22
23
  import vn.momo.kits.application.IsShowBaseLineDebug
23
24
  import vn.momo.kits.const.Colors
@@ -68,7 +69,7 @@ fun TrustBanner(
68
69
  trackEvent: ((String, Map<String, String>) -> Unit)? = null
69
70
  ) {
70
71
  val localize = LocalLocalize.current
71
- val trustBanner = defaultBanner
72
+ val trustBanner = LocalDesignSystemConfig.current?.trustBanner ?: defaultBanner
72
73
  val trackParams = mapOf(
73
74
  "service_name" to serviceName,
74
75
  "screen_name" to screenName,
@@ -33,6 +33,7 @@ class Navigation(
33
33
  tintColor: Color? = null,
34
34
  footerComponent: (@Composable () -> Unit)? = null,
35
35
  footerTransparent: Boolean? = null,
36
+ footerUseMargin: Boolean? = null,
36
37
  floatingButtonProps: FloatingButtonProps? = null,
37
38
  keyboardOptions: KeyboardOptions? = null
38
39
  ) {
@@ -50,6 +51,7 @@ class Navigation(
50
51
  tintColor = tintColor ?: options.tintColor,
51
52
  footerComponent = footerComponent ?: options.footerComponent,
52
53
  footerTransparent = footerTransparent ?: options.footerTransparent,
54
+ footerUseMargin = footerUseMargin ?: options.footerUseMargin,
53
55
  floatingButtonProps = floatingButtonProps ?: options.floatingButtonProps,
54
56
  keyboardOptions = keyboardOptions ?: options.keyboardOptions,
55
57
  )
@@ -86,6 +88,7 @@ data class NavigationOptions(
86
88
  val tintColor: Color? = null,
87
89
  val footerComponent: @Composable (() -> Unit)? = null,
88
90
  val footerTransparent: Boolean = false,
91
+ val footerUseMargin: Boolean = true,
89
92
  val floatingButtonProps: FloatingButtonProps? = null,
90
93
  val keyboardOptions: KeyboardOptions = KeyboardOptions()
91
94
  ) {
@@ -2,9 +2,7 @@ package vn.momo.kits.navigation
2
2
 
3
3
  import androidx.compose.animation.core.tween
4
4
  import androidx.compose.animation.fadeIn
5
- import androidx.compose.animation.slideInHorizontally
6
5
  import androidx.compose.animation.slideInVertically
7
- import androidx.compose.animation.slideOutHorizontally
8
6
  import androidx.compose.animation.slideOutVertically
9
7
  import androidx.compose.runtime.Composable
10
8
  import androidx.compose.runtime.CompositionLocalProvider
@@ -19,7 +17,12 @@ import androidx.navigation.compose.NavHost
19
17
  import androidx.navigation.compose.composable
20
18
  import androidx.navigation.compose.rememberNavController
21
19
  import androidx.navigation.toRoute
20
+ import kotlinx.coroutines.delay
21
+ import kotlinx.coroutines.flow.collectLatest
22
+ import vn.momo.kits.application.DesignSystemConfig
23
+ import vn.momo.kits.application.KitDesignSystemConfig
22
24
  import vn.momo.kits.application.LocalContext
25
+ import vn.momo.kits.application.LocalDesignSystemConfig
23
26
  import vn.momo.kits.application.LocalLocalize
24
27
  import vn.momo.kits.application.Localize
25
28
  import vn.momo.kits.application.MiniAppContext
@@ -33,6 +36,7 @@ import vn.momo.kits.platform.ProvideOsFontScale
33
36
  import vn.momo.kits.utils.getAppStatusBarHeight
34
37
  import vn.momo.kits.utils.getNavigationBarHeight
35
38
  import vn.momo.maxapi.IMaxApi
39
+ import kotlin.time.Duration.Companion.milliseconds
36
40
 
37
41
  @Composable
38
42
  fun NavigationContainer(
@@ -45,6 +49,7 @@ fun NavigationContainer(
45
49
  setNavigator: ((Navigator) -> Unit)? = null,
46
50
  statusBarHeight: Dp? = null,
47
51
  localize: Localize? = null,
52
+ designSystemConfig: DesignSystemConfig? = null,
48
53
  ) {
49
54
  val navController = rememberNavController()
50
55
  val registry = remember { DynamicScreenRegistry() }
@@ -55,7 +60,19 @@ fun NavigationContainer(
55
60
  val parentContext = LocalContext.current
56
61
  val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
57
62
 
63
+ val stackTransition = remember { StackTransitionCoordinator() }
58
64
  val theme = remember { mutableStateOf(initialTheme) }
65
+ val resolvedDesignSystemConfig =
66
+ designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
67
+ val headerBar = resolvedDesignSystemConfig?.headerBar
68
+ // RN parity: the theme's own header image wins; config headerBar is only the fallback.
69
+ val resolvedTheme = remember(theme.value, headerBar) {
70
+ if (theme.value.isHeaderImage() || headerBar.isNullOrEmpty()) {
71
+ theme.value
72
+ } else {
73
+ theme.value.copy(assets = theme.value.assets.copy(headerBackground = headerBar))
74
+ }
75
+ }
59
76
  val fallbackLocalize = remember { Localize() }
60
77
 
61
78
  val resolvedLocalize = localize ?: fallbackLocalize
@@ -81,43 +98,59 @@ fun NavigationContainer(
81
98
  LocalNavigator provides navigator,
82
99
  LocalDynamicScreenRegistry provides registry,
83
100
  LocalMaxApi provides maxApi,
84
- AppTheme provides theme.value,
101
+ AppTheme provides resolvedTheme,
85
102
  AppStatusBar provides statusBarHeight,
86
103
  AppNavigationBar provides navigationBarHeight,
87
104
  LocalContext provides mergedContext,
105
+ LocalDesignSystemConfig provides resolvedDesignSystemConfig,
88
106
  LocalLocalize provides resolvedLocalize,
107
+ LocalStackTransition provides stackTransition,
89
108
  ) {
90
109
  LaunchedEffect(Unit) {
91
110
  setNavigator?.invoke(navigator)
92
111
  }
93
112
 
94
- NavHost(navController, startDestination = startDestination) {
95
- composable<DynamicScreenRoute>(
96
- enterTransition = {
97
- slideInHorizontally(
98
- animationSpec = tween(300),
99
- initialOffsetX = { it }
100
- )
101
- },
102
- exitTransition = null,
103
- popEnterTransition = { fadeIn(animationSpec = tween(0)) },
104
- popExitTransition = {
105
- slideOutHorizontally(
106
- animationSpec = tween(300),
107
- targetOffsetX = { it }
108
- )
113
+ SwipeBackHaptic(maxApi)
114
+
115
+ // Gesture pops never reach Navigator.dismissScreen(), so prune the registry from
116
+ // the back stack itself. collectLatest lets a rapid re-push cancel a pending prune.
117
+ LaunchedEffect(navController, registry) {
118
+ navController.currentBackStack.collectLatest { entries ->
119
+ delay(STACK_TRANSITION_DURATION.milliseconds)
120
+ val aliveIds = entries.mapNotNullTo(mutableSetOf()) { entry ->
121
+ runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
122
+ ?: runCatching { entry.toRoute<DynamicDialogRoute>().id }.getOrNull()
109
123
  }
110
- ) { backStackEntry ->
124
+ aliveIds.add(screenId)
125
+ registry.retainOnly(aliveIds)
126
+ }
127
+ }
128
+
129
+ // The transitions must live on the NavHost call site, not on the destinations:
130
+ // NavHost.uikit computes isDefaultTransition from these arguments alone, and while
131
+ // it is true the library paints its own hard-coded black iOS blackout that would
132
+ // sit over the pink scrim.
133
+ NavHost(
134
+ navController,
135
+ startDestination = startDestination,
136
+ enterTransition = { stackEnter() },
137
+ exitTransition = { stackExit() },
138
+ popEnterTransition = { stackPopEnter() },
139
+ popExitTransition = { stackPopExit() },
140
+ ) {
141
+ composable<DynamicScreenRoute> { backStackEntry ->
111
142
  val route = backStackEntry.toRoute<DynamicScreenRoute>()
112
143
  val screen = registry.getScreen(route.id)
113
144
 
114
145
  if (screen != null) {
115
- StackScreen(
116
- id = route.id,
117
- name = screen.name,
118
- content = screen.content,
119
- navigationOptions = screen.options
120
- )
146
+ StackTransitionDecor(id = route.id, isStackRoute = true) {
147
+ StackScreen(
148
+ id = route.id,
149
+ name = screen.name,
150
+ content = screen.content,
151
+ navigationOptions = screen.options
152
+ )
153
+ }
121
154
  }
122
155
  }
123
156
 
@@ -141,12 +174,14 @@ fun NavigationContainer(
141
174
  val screen = registry.getScreen(route.id)
142
175
 
143
176
  if (screen != null) {
144
- StackScreen(
145
- id = route.id,
146
- name = screen.name,
147
- content = screen.content,
148
- navigationOptions = screen.options
149
- )
177
+ StackTransitionDecor(id = route.id, isStackRoute = false) {
178
+ StackScreen(
179
+ id = route.id,
180
+ name = screen.name,
181
+ content = screen.content,
182
+ navigationOptions = screen.options
183
+ )
184
+ }
150
185
  }
151
186
  }
152
187
  }