@momo-kits/native-kits 0.162.34-debug → 0.162.35-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 (49) hide show
  1. package/CLAUDE.md +2 -0
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -1
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +18 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -1
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +30 -4
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +3 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +8 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +9 -4
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +4 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +3 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +104 -22
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +8 -4
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +2 -1
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +2 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +38 -5
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +62 -33
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +22 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +25 -5
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +19 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +13 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +3 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +5 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -1
  37. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  38. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  39. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +10 -4
  40. package/gradle/libs.versions.toml +1 -1
  41. package/gradle.properties +1 -1
  42. package/ios/Application/Navigation/NavigatorManager.swift +0 -27
  43. package/ios/Colors+Radius+Spacing/Theme.swift +2 -2
  44. package/ios/Typography/KitDesignSystem.swift +17 -0
  45. package/ios/Typography/Text.swift +25 -1
  46. package/ios/native-kits.podspec +1 -1
  47. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  48. package/package.json +1 -1
  49. package/publish.sh +25 -0
package/CLAUDE.md CHANGED
@@ -41,9 +41,11 @@ 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`.
48
+ - **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
47
49
 
48
50
  ## i18n / Localize (port of RN `Localize`)
49
51
 
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.162.34-debug"
43
+ version = "0.162.35-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-beta.2-debug'
3
+ spec.version = '0.164.1-swipeback.1'
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
@@ -30,6 +30,7 @@ import com.airbnb.lottie.compose.rememberLottieDynamicProperties
30
30
  import com.airbnb.lottie.compose.rememberLottieDynamicProperty
31
31
  import vn.momo.kits.const.AppNavigationBar
32
32
  import vn.momo.kits.const.AppStatusBar
33
+ import vn.momo.kits.navigation.Navigator
33
34
  import vn.momo.kits.utils.readJson
34
35
  import androidx.activity.compose.BackHandler as AndroidBackHandler
35
36
 
@@ -64,7 +65,7 @@ actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
64
65
  }
65
66
 
66
67
  @Composable
67
- actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
68
+ actual fun ProvideNavigationEventDispatcherOwner(navigator: Navigator, content: @Composable () -> Unit) {
68
69
  content()
69
70
  }
70
71
 
@@ -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 }
@@ -40,9 +40,11 @@ import vn.momo.kits.const.AppTheme
40
40
  import vn.momo.kits.const.Colors
41
41
  import vn.momo.kits.const.Spacing
42
42
  import vn.momo.kits.const.Typography
43
+ import vn.momo.kits.const.getFontFamily
43
44
  import vn.momo.kits.modifier.kitsAutomationId
44
45
  import vn.momo.kits.modifier.noFeedbackClickable
45
46
  import vn.momo.kits.modifier.setAutomationId
47
+ import vn.momo.kits.navigation.component.HEADER_FONT_FAMILY
46
48
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
47
49
  import vn.momo.kits.utils.getAppStatusBarHeight
48
50
  import kotlin.math.max
@@ -360,6 +362,7 @@ private fun LiteScreenLayout(
360
362
  text = title,
361
363
  color = headerColor.tintIconColor,
362
364
  style = titleStyle,
365
+ fontFamily = HEADER_FONT_FAMILY,
363
366
  modifier = titleModifier,
364
367
  maxLines = 1,
365
368
  overflow = TextOverflow.Ellipsis
@@ -688,9 +691,11 @@ private fun LiteInputSearch(
688
691
  inputSearchProps.isShowBtnText && !inputSearchProps.btnText.isNullOrEmpty()
689
692
  }
690
693
  }
691
- val inputFieldStyle = remember(theme) {
694
+ val inputFieldFontFamily = getFontFamily(theme.font, Typography.bodyDefaultRegular.fontWeight)
695
+ val inputFieldStyle = remember(theme, inputFieldFontFamily) {
692
696
  Typography.bodyDefaultRegular.copy(
693
697
  color = theme.colors.text.default,
698
+ fontFamily = inputFieldFontFamily,
694
699
  )
695
700
  }
696
701
 
@@ -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
+ }
@@ -266,7 +266,7 @@ fun Input(
266
266
  )
267
267
  }
268
268
 
269
- val passwordFontFamily = getFontFamily("SFProText", FontWeight.Normal)
269
+ val passwordFontFamily = getFontFamily(AppTheme.current.font, FontWeight.Normal)
270
270
  val passwordFontSize = scaleSize(24.sp)
271
271
  val visualTransformation = remember(secureTextEntry, inputState.passHidden, passwordFontFamily) {
272
272
  if (secureTextEntry && !inputState.passHidden)
@@ -44,6 +44,7 @@ import vn.momo.kits.const.Spacing
44
44
  import vn.momo.kits.application.IsShowBaseLineDebug
45
45
  import vn.momo.kits.const.Colors
46
46
  import vn.momo.kits.const.Typography
47
+ import vn.momo.kits.const.getFontFamily
47
48
  import vn.momo.kits.modifier.conditional
48
49
  import vn.momo.kits.utils.formatNumberToMoney
49
50
 
@@ -122,7 +123,8 @@ fun InputMoney(
122
123
  color = textColor,
123
124
  fontSize = 20.sp,
124
125
  lineHeight = 24.sp,
125
- fontWeight = fontWeight.value
126
+ fontWeight = fontWeight.value,
127
+ fontFamily = getFontFamily(AppTheme.current.font, fontWeight.value)
126
128
  ),
127
129
  visualTransformation = CustomConverter(),
128
130
  keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.Dp
21
21
  import androidx.compose.ui.unit.dp
22
22
  import vn.momo.kits.application.IsShowBaseLineDebug
23
23
  import vn.momo.kits.const.*
24
+ import vn.momo.kits.const.getFontFamily
24
25
  import vn.momo.kits.modifier.conditional
25
26
  import vn.momo.kits.modifier.setAutomationId
26
27
 
@@ -97,7 +98,13 @@ fun InputPhoneNumber(
97
98
  }
98
99
 
99
100
  val (textColor, placeholderColor, iconTintColor) = colors
100
- val textStyle = scaleSize(size.values.textStyle.copy(color = textColor))
101
+ val baseTextStyle = size.values.textStyle
102
+ val textStyle = scaleSize(
103
+ baseTextStyle.copy(
104
+ color = textColor,
105
+ fontFamily = getFontFamily(AppTheme.current.font, baseTextStyle.fontWeight)
106
+ )
107
+ )
101
108
 
102
109
 
103
110
  Column(modifier = modifier
@@ -42,6 +42,7 @@ import vn.momo.kits.const.Radius
42
42
  import vn.momo.kits.const.Spacing
43
43
  import vn.momo.kits.application.IsShowBaseLineDebug
44
44
  import vn.momo.kits.const.Typography
45
+ import vn.momo.kits.const.getFontFamily
45
46
  import vn.momo.kits.const.scaleSize
46
47
  import vn.momo.kits.modifier.conditional
47
48
  import vn.momo.kits.utils.ifTrue
@@ -131,19 +132,23 @@ fun InputSearch(
131
132
  val fontSize = 14.sp
132
133
  val scaleFontSize = scaleSize(fontSize)
133
134
 
134
- val textStyle = remember(textColor, inputSearchProps.fontWeight.value) {
135
+ val fontFamily = getFontFamily(AppTheme.current.font, inputSearchProps.fontWeight.value)
136
+
137
+ val textStyle = remember(textColor, inputSearchProps.fontWeight.value, fontFamily) {
135
138
  TextStyle(
136
139
  color = textColor,
137
140
  fontSize = scaleFontSize,
138
- fontWeight = inputSearchProps.fontWeight.value
141
+ fontWeight = inputSearchProps.fontWeight.value,
142
+ fontFamily = fontFamily
139
143
  )
140
144
  }
141
145
 
142
- val placeHolderStyle = remember(textColor, inputSearchProps.fontWeight.value) {
146
+ val placeHolderStyle = remember(textColor, inputSearchProps.fontWeight.value, fontFamily) {
143
147
  TextStyle(
144
148
  color = textColor,
145
149
  fontSize = fontSize,
146
- fontWeight = inputSearchProps.fontWeight.value
150
+ fontWeight = inputSearchProps.fontWeight.value,
151
+ fontFamily = fontFamily
147
152
  )
148
153
  }
149
154
 
@@ -21,6 +21,8 @@ import androidx.compose.ui.unit.sp
21
21
  import androidx.compose.ui.zIndex
22
22
  import vn.momo.kits.application.IsShowBaseLineDebug
23
23
  import vn.momo.kits.const.*
24
+ import vn.momo.kits.const.AppTheme
25
+ import vn.momo.kits.const.getFontFamily
24
26
  import vn.momo.kits.modifier.InternalApi
25
27
  import vn.momo.kits.modifier.conditional
26
28
 
@@ -75,7 +77,8 @@ fun InputTextArea(
75
77
  color = textColor,
76
78
  fontSize = scaleFontSize,
77
79
  lineHeight = scaleLineHeight,
78
- fontWeight = fontWeight.value
80
+ fontWeight = fontWeight.value,
81
+ fontFamily = getFontFamily(AppTheme.current.font, fontWeight.value)
79
82
  ),
80
83
  keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
81
84
  modifier = Modifier.height(scaleHeight).onFocusChanged {
@@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment
12
12
  import androidx.compose.ui.Modifier
13
13
  import androidx.compose.ui.graphics.Color
14
14
  import androidx.compose.ui.text.TextStyle
15
+ import androidx.compose.ui.text.font.FontWeight
15
16
  import androidx.compose.ui.text.input.KeyboardType
16
17
  import androidx.compose.ui.text.style.TextAlign
17
18
  import androidx.compose.ui.unit.Dp
@@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp
19
20
  import androidx.compose.ui.unit.sp
20
21
  import vn.momo.kits.application.IsShowBaseLineDebug
21
22
  import vn.momo.kits.const.*
23
+ import vn.momo.kits.const.getFontFamily
22
24
  import vn.momo.kits.modifier.activeOpacityClickable
23
25
  import vn.momo.kits.modifier.conditional
24
26
 
@@ -108,6 +110,7 @@ private fun StepperNumberView(
108
110
  color = textColor,
109
111
  fontSize = scaledFontSize,
110
112
  textAlign = TextAlign.Center,
113
+ fontFamily = getFontFamily(theme.font, FontWeight.Normal),
111
114
  )
112
115
 
113
116
  Box(
@@ -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,
@@ -0,0 +1,18 @@
1
+ package vn.momo.kits.const
2
+
3
+ import androidx.compose.runtime.getValue
4
+ import androidx.compose.runtime.mutableStateOf
5
+ import androidx.compose.runtime.setValue
6
+
7
+ // Design-system rollout holder, same shape as KitFontScale: the kit is sandboxed and can't read the
8
+ // host config, so the host seeds this once at startup from DesignSystemConfig. getFontFamily() reads
9
+ // it to decide whether the new default font applies, so every screen follows the flag with no prop
10
+ // threading. Backed by snapshot state: writes recompose text automatically.
11
+ object KitDesignSystem {
12
+ var isWhiteList by mutableStateOf(false)
13
+ private set
14
+
15
+ fun update(isWhiteList: Boolean) {
16
+ this.isWhiteList = isWhiteList
17
+ }
18
+ }
@@ -117,7 +117,7 @@ val defaultTheme = Theme(
117
117
  container = Colors.blue_08
118
118
  )
119
119
  ),
120
- font = "SFProText",
120
+ font = "MomoTrustSans",
121
121
  assets = ThemeAssets(
122
122
  headerBackground = null
123
123
  )
@@ -174,7 +174,7 @@ val darkTheme = Theme(
174
174
  container = Colors.blue_08
175
175
  )
176
176
  ),
177
- font = "SFProText",
177
+ font = "MomoTrustSans",
178
178
  assets = ThemeAssets(
179
179
  headerBackground = null
180
180
  )
@@ -89,18 +89,29 @@ fun scaleSize(textStyle: TextStyle): TextStyle {
89
89
  * SDK không đóng gói font riêng nữa: app host đã bundle sẵn toàn bộ font từ `font-source`
90
90
  * cho cả React Native lẫn native, nên bản copy trong `composeResources` chỉ làm phình app.
91
91
  */
92
- private const val DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
92
+ private const val LEGACY_DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
93
+ private const val NEW_DEFAULT_FONT_FILE = "MomoTrustSans-Regular.ttf"
93
94
 
94
95
  private val hostFontFiles: Map<String, String> = mapOf(
95
96
  "SFProText-100" to "SFProText-Thin.ttf",
96
97
  "SFProText-200" to "SFProText-Ultralight.ttf",
97
98
  "SFProText-300" to "SFProText-Light.ttf",
98
- "SFProText-400" to DEFAULT_FONT_FILE,
99
+ "SFProText-400" to LEGACY_DEFAULT_FONT_FILE,
99
100
  "SFProText-500" to "SFProText-Medium.ttf",
100
101
  "SFProText-600" to "SFProText-Semibold.ttf",
101
102
  "SFProText-700" to "SFProText-Bold.ttf",
102
103
  "SFProText-800" to "SFProText-Heavy.ttf",
103
104
  "SFProText-900" to "SFProText-Black.ttf",
105
+
106
+ "MomoTrustSans-100" to "MomoTrustSans-ExtraLight.ttf",
107
+ "MomoTrustSans-200" to "MomoTrustSans-ExtraLight.ttf",
108
+ "MomoTrustSans-300" to "MomoTrustSans-Light.ttf",
109
+ "MomoTrustSans-400" to "MomoTrustSans-Regular.ttf",
110
+ "MomoTrustSans-500" to "MomoTrustSans-Medium.ttf",
111
+ "MomoTrustSans-600" to "MomoTrustSans-SemiBold.ttf",
112
+ "MomoTrustSans-700" to "MomoTrustSans-Bold.ttf",
113
+ "MomoTrustSans-800" to "MomoTrustSans-ExtraBold.ttf",
114
+ "MomoTrustSans-900" to "MomoTrustSans-ExtraBold.ttf",
104
115
  ) + (1..9).flatMap { step ->
105
116
  val weight = step * 100
106
117
  listOf(
@@ -109,12 +120,34 @@ private val hostFontFiles: Map<String, String> = mapOf(
109
120
  )
110
121
  }
111
122
 
123
+ /**
124
+ * Font mặc định mới, chỉ áp cho user nằm trong whitelist design system ([KitDesignSystem]).
125
+ * Ngoài whitelist thì quay về [LEGACY_DEFAULT_FONT_FAMILY] như trước.
126
+ */
127
+ private const val NEW_DEFAULT_FONT_FAMILY = "MomoTrustSans"
128
+ private const val LEGACY_DEFAULT_FONT_FAMILY = "SFProText"
129
+
130
+ /**
131
+ * Font thuoc bo design system moi. MoMoTrustDisplay chi duoc header dung tu DS-791
132
+ * (commit header title 5bfde6251); truoc do header lay font theo theme, nen ngoai
133
+ * whitelist ca hai deu ha ve [LEGACY_DEFAULT_FONT_FAMILY].
134
+ */
135
+ private val newFontFamilies = setOf(NEW_DEFAULT_FONT_FAMILY, "MoMoTrustDisplay")
136
+
112
137
  @Composable
113
138
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
114
- val key = "$fontFamily-${fontWeight?.weight}"
115
- val fileName = hostFontFiles[key] ?: DEFAULT_FONT_FILE
139
+ val isWhiteList = KitDesignSystem.isWhiteList
140
+ val resolvedFamily =
141
+ if (fontFamily in newFontFamilies && !isWhiteList) {
142
+ LEGACY_DEFAULT_FONT_FAMILY
143
+ } else {
144
+ fontFamily
145
+ }
146
+ val defaultFile = if (isWhiteList) NEW_DEFAULT_FONT_FILE else LEGACY_DEFAULT_FONT_FILE
147
+ val key = "$resolvedFamily-${fontWeight?.weight}"
148
+ val fileName = hostFontFiles[key] ?: defaultFile
116
149
  return hostFontFamily(fileName)
117
- ?: hostFontFamily(DEFAULT_FONT_FILE)
150
+ ?: hostFontFamily(defaultFile)
118
151
  ?: FontFamily.Default
119
152
  }
120
153