@momo-kits/native-kits 0.167.1-beta.20-debug → 0.167.1-beta.21-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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-beta.20-debug"
43
+ version = "0.167.1-beta.21-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -188,11 +188,3 @@ compose {
188
188
  // }
189
189
  // }
190
190
  // }
191
-
192
- // Xcode 27 rejects iOS < 15; KGP's synthetic Podfile only raises pods to 12 (KT-57741), leaving lottie-ios at 13.
193
- tasks.named("podGenIos") {
194
- doLast {
195
- val podfile = layout.buildDirectory.file("cocoapods/synthetic/ios/Podfile").get().asFile
196
- podfile.writeText(podfile.readText().replace("major < 12 ||", "major < 15 ||").replace("#{12}", "#{15}"))
197
- }
198
- }
@@ -188,11 +188,3 @@ mavenPublishing {
188
188
  }
189
189
  }
190
190
  }
191
-
192
- // Xcode 27 rejects iOS < 15; KGP's synthetic Podfile only raises pods to 12 (KT-57741), leaving lottie-ios at 13.
193
- tasks.named("podGenIos") {
194
- doLast {
195
- val podfile = layout.buildDirectory.file("cocoapods/synthetic/ios/Podfile").get().asFile
196
- podfile.writeText(podfile.readText().replace("major < 12 ||", "major < 15 ||").replace("#{12}", "#{15}"))
197
- }
198
- }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.167.1-beta.20-debug'
3
+ spec.version = '0.167.1-beta.21-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -67,7 +67,6 @@ import vn.momo.kits.navigation.component.HeaderBackground
67
67
  import vn.momo.kits.navigation.component.HeaderRight
68
68
  import vn.momo.kits.navigation.component.HeaderType
69
69
  import vn.momo.kits.navigation.component.InputSearchType
70
- import vn.momo.kits.navigation.bottomtab.LocalBottomTabFocused
71
70
  import vn.momo.kits.platform.BackHandler
72
71
  import vn.momo.kits.platform.supportsImePadding
73
72
  import vn.momo.kits.navigation.tracking.ScreenTracker
@@ -110,24 +109,20 @@ internal fun StackScreen(
110
109
  val isBottomTabChild = bottomTabIndex != -1
111
110
  val isBottomTabRoot = navigation.isBottomTabRoot
112
111
 
113
- // A mounted-but-hidden bottom tab tracks again each time it is focused (RN useFocusEffect).
114
- val tabFocused = LocalBottomTabFocused.current
115
-
116
112
  // Auto tracking state
117
- val trackingState = remember(tabFocused) {
113
+ val trackingState = remember {
118
114
  ScreenTrackingState().apply {
119
115
  time = Clock.System.now().toEpochMilliseconds()
120
116
  }
121
117
  }
122
118
 
123
119
  // Determine action: push or back
124
- val action = remember(tabFocused) {
120
+ val action = remember {
125
121
  if (ScreenTracker.getLastScreenName() != null) "push" else "back"
126
122
  }
127
123
 
128
124
  // Auto tracking effects
129
- LaunchedEffect(name, tabFocused) {
130
- if (!tabFocused) return@LaunchedEffect
125
+ LaunchedEffect(name) {
131
126
  // Track screen navigated immediately
132
127
  ScreenTracker.trackScreenNavigated(
133
128
  maxApi = maxApi,
@@ -174,7 +169,7 @@ internal fun StackScreen(
174
169
  val footerHeightPx = remember { mutableIntStateOf(0) }
175
170
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
176
171
 
177
- if (tabFocused && navigation.options.scrollData.scrollToTopCallback != null) {
172
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
178
173
  RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
179
174
  }
180
175
 
@@ -1,34 +1,27 @@
1
1
  package vn.momo.kits.navigation.bottomtab
2
2
 
3
- import androidx.compose.animation.core.Animatable
4
- import androidx.compose.animation.core.AnimationVector1D
5
- import androidx.compose.animation.core.Easing
6
3
  import androidx.compose.animation.core.tween
4
+ import androidx.compose.animation.fadeIn
5
+ import androidx.compose.animation.fadeOut
6
+ import androidx.compose.animation.scaleIn
7
+ import androidx.compose.animation.scaleOut
7
8
  import androidx.compose.foundation.background
8
9
  import androidx.compose.foundation.layout.*
9
10
  import androidx.compose.runtime.Composable
10
- import androidx.compose.runtime.CompositionLocalProvider
11
11
  import androidx.compose.runtime.DisposableEffect
12
12
  import androidx.compose.runtime.LaunchedEffect
13
- import androidx.compose.runtime.getValue
14
- import androidx.compose.runtime.key
15
- import androidx.compose.runtime.mutableIntStateOf
16
- import androidx.compose.runtime.mutableStateOf
17
13
  import androidx.compose.runtime.remember
18
- import androidx.compose.runtime.saveable.rememberSaveable
19
- import androidx.compose.runtime.saveable.rememberSaveableStateHolder
20
- import androidx.compose.runtime.setValue
21
- import androidx.compose.runtime.staticCompositionLocalOf
22
14
  import androidx.compose.ui.Alignment
23
15
  import androidx.compose.ui.Modifier
24
- import androidx.compose.ui.graphics.graphicsLayer
25
- import androidx.compose.ui.layout.layout
26
- import androidx.compose.ui.zIndex
16
+ import androidx.compose.ui.unit.dp
17
+ import androidx.navigation.compose.NavHost
18
+ import androidx.navigation.compose.composable
19
+ import androidx.navigation.compose.currentBackStackEntryAsState
20
+ import androidx.navigation.compose.rememberNavController
27
21
  import vn.momo.kits.const.AppNavigationBar
28
22
  import vn.momo.kits.const.AppTheme
29
23
  import vn.momo.kits.const.Spacing
30
24
  import vn.momo.kits.navigation.KeyboardOptions
31
- import vn.momo.kits.navigation.LocalMaxApi
32
25
  import vn.momo.kits.navigation.LocalNavigation
33
26
  import vn.momo.kits.navigation.LocalNavigator
34
27
  import vn.momo.kits.navigation.NavigationOptions
@@ -36,17 +29,6 @@ import vn.momo.kits.navigation.StackScreen
36
29
  import vn.momo.kits.navigation.component.HeaderType
37
30
  import vn.momo.kits.platform.BackHandler
38
31
  import vn.momo.kits.platform.reportFloatingArrowBack
39
- import kotlinx.coroutines.launch
40
-
41
- private val inOutQuad = Easing { t -> if (t < 0.5f) 2 * t * t else 1 - 2 * (1 - t) * (1 - t) }
42
- internal val tabTimingSpec = tween<Float>(durationMillis = 200, easing = inOutQuad)
43
-
44
- internal val LocalBottomTabFocused = staticCompositionLocalOf { true }
45
-
46
- private val Hidden = Modifier.layout { measurable, constraints ->
47
- val placeable = measurable.measure(constraints)
48
- layout(placeable.width, placeable.height) {}
49
- }
50
32
 
51
33
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
52
34
  internal fun setBottomTabOption(index: Int, options: NavigationOptions){
@@ -70,8 +52,10 @@ fun BottomTab(
70
52
  ) {
71
53
  val navigation = LocalNavigation.current
72
54
  val navigator = LocalNavigator.current
73
- val maxApi = LocalMaxApi.current
74
- val startIndex = remember { items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0) }
55
+ val navController = rememberNavController()
56
+ val startRoute = remember {
57
+ "option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
58
+ }
75
59
 
76
60
  bottomTabOptionItems = items.mapIndexed { index, item ->
77
61
  item.options ?: NavigationOptions()
@@ -86,34 +70,14 @@ fun BottomTab(
86
70
  )
87
71
  }
88
72
 
89
- var selectedIndex by rememberSaveable { mutableIntStateOf(startIndex) }
90
- var loadedTabs by rememberSaveable { mutableStateOf(listOf(startIndex)) }
91
- val tabStateHolder = rememberSaveableStateHolder()
92
- val selectTab: (Int) -> Unit = { index ->
93
- if (index !in loadedTabs) loadedTabs = loadedTabs + index
94
- selectedIndex = index
95
- }
96
-
97
- val onFirstTab = selectedIndex == 0
98
- val isFirstTab = onFirstTab || !backToInitialTab
99
- val backToFirstTab = { selectTab(0) }
100
-
101
- var lastFocusedIndex by rememberSaveable { mutableStateOf<Int?>(null) }
102
- val tabProgress = remember { mutableMapOf<Int, Animatable<Float, AnimationVector1D>>() }
103
- fun progressOf(index: Int) = tabProgress.getOrPut(index) {
104
- Animatable(if (index == lastFocusedIndex) 1f else 0f)
105
- }
106
- LaunchedEffect(selectedIndex) {
107
- lastFocusedIndex = selectedIndex
108
- tabProgress.forEach { (index, progress) ->
109
- if (index != selectedIndex) launch { progress.animateTo(0f, tabTimingSpec) }
110
- }
111
- progressOf(selectedIndex).animateTo(1f, tabTimingSpec)
112
- }
73
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
74
+ val onStartTab = currentRoute == null || currentRoute == startRoute
75
+ val isFirstTab = onStartTab || !backToInitialTab
76
+ val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
113
77
 
114
78
  DisposableEffect(isFirstTab) {
115
79
  reportFloatingArrowBack(!isFirstTab)
116
- navigator.tabBackHandler = if (isFirstTab) null else ({ backToFirstTab() })
80
+ navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
117
81
  // both live outside this composition
118
82
  onDispose {
119
83
  reportFloatingArrowBack(false)
@@ -124,56 +88,75 @@ fun BottomTab(
124
88
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
125
89
  Box(modifier = Modifier
126
90
  .fillMaxSize()
127
- .background(AppTheme.current.colors.background.default)
128
91
  ) {
129
- loadedTabs.forEach { index ->
130
- val item = items.getOrNull(index) ?: return@forEach
131
- val focused = index == selectedIndex
132
- val progress = progressOf(index)
133
- key(index) {
134
- Box(
135
- Modifier
136
- .fillMaxSize()
137
- .zIndex(if (focused) 0f else -1f)
138
- .then(if (focused) Modifier.graphicsLayer {
139
- alpha = 0.3f + 0.7f * progress.value
140
- scaleX = 0.97f + 0.03f * progress.value
141
- scaleY = scaleX
142
- } else Hidden)
92
+ NavHost(
93
+ navController = navController,
94
+ startDestination = startRoute
95
+ ) {
96
+ items.forEachIndexed { index, item ->
97
+ composable(
98
+ route = "option$index",
99
+ enterTransition = {
100
+ fadeIn(animationSpec = tween(200)) +
101
+ scaleIn(
102
+ initialScale = 0.97f,
103
+ animationSpec = tween(200)
104
+ )
105
+ },
106
+ exitTransition = {
107
+ fadeOut(animationSpec = tween(200)) +
108
+ scaleOut(
109
+ targetScale = 0.97f,
110
+ animationSpec = tween(200)
111
+ )
112
+ },
113
+ popEnterTransition = {
114
+ fadeIn(animationSpec = tween(200)) +
115
+ scaleIn(
116
+ initialScale = 0.97f,
117
+ animationSpec = tween(200)
118
+ )
119
+ },
120
+ popExitTransition = {
121
+ fadeOut(animationSpec = tween(200)) +
122
+ scaleOut(
123
+ targetScale = 0.97f,
124
+ animationSpec = tween(200)
125
+ )
126
+ }
143
127
  ) {
144
- CompositionLocalProvider(LocalBottomTabFocused provides focused) {
145
- tabStateHolder.SaveableStateProvider(index) {
146
- StackScreen(
147
- content = item.screen,
148
- navigationOptions = getBottomTabOption(index),
149
- bottomTabIndex = index,
150
- onBackHandler = {
151
- if (backToInitialTab && index != 0) {
152
- backToFirstTab()
153
- } else {
154
- navigator.pop()
155
- }
156
- }
157
- )
128
+ val option = getBottomTabOption(index)
129
+
130
+ StackScreen(
131
+ content = item.screen,
132
+ navigationOptions = option,
133
+ bottomTabIndex = index,
134
+ onBackHandler = {
135
+ if (backToInitialTab && "option$index" != startRoute) {
136
+ backToStartTab()
137
+ } else {
138
+ navigator.pop()
139
+ }
158
140
  }
159
- }
141
+ )
160
142
  }
161
143
  }
162
144
  }
163
145
  }
164
- BackHandler(enabled = !onFirstTab) {
165
- if (backToInitialTab) backToFirstTab() else navigator.pop()
146
+ BackHandler(enabled = !onStartTab) {
147
+ if (backToInitialTab) backToStartTab() else navigator.pop()
166
148
  }
167
149
  Column {
168
150
  BottomTabBar(
169
151
  items = items,
170
152
  floatingButton = floatingButton,
171
- selectedIndex = selectedIndex,
153
+ navController = navController,
172
154
  onTabSelected = { index ->
173
- // RN parity: every tab press (focused one included) ticks a light haptic.
174
- runCatching { maxApi?.triggerEventVibration("light", callback = null) }
175
155
  val switchTab = {
176
- if (index != selectedIndex) selectTab(index)
156
+ val targetRoute = "option$index"
157
+ if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
158
+ navController.navigate(targetRoute)
159
+ }
177
160
  }
178
161
  onTabPress?.invoke(index, switchTab) ?: switchTab()
179
162
  }
@@ -1,18 +1,15 @@
1
1
  package vn.momo.kits.navigation.bottomtab
2
2
 
3
- import androidx.compose.animation.core.Animatable
3
+ import androidx.compose.animation.core.FastOutSlowInEasing
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.animation.core.tween
4
6
  import androidx.compose.foundation.background
5
7
  import androidx.compose.foundation.border
6
8
  import androidx.compose.foundation.layout.*
7
9
  import androidx.compose.foundation.shape.CircleShape
8
10
  import androidx.compose.foundation.shape.RoundedCornerShape
9
11
  import androidx.compose.runtime.Composable
10
- import androidx.compose.runtime.LaunchedEffect
11
12
  import androidx.compose.runtime.getValue
12
- import androidx.compose.runtime.mutableIntStateOf
13
- import androidx.compose.runtime.remember
14
- import androidx.compose.runtime.saveable.rememberSaveable
15
- import androidx.compose.runtime.setValue
16
13
  import androidx.compose.ui.Alignment
17
14
  import androidx.compose.ui.Modifier
18
15
  import androidx.compose.ui.draw.clip
@@ -20,17 +17,18 @@ import androidx.compose.ui.graphics.Brush
20
17
  import androidx.compose.ui.graphics.Color
21
18
  import androidx.compose.ui.text.style.TextAlign
22
19
  import androidx.compose.ui.text.style.TextOverflow
23
- import androidx.compose.ui.unit.Dp
24
- import androidx.compose.ui.unit.IntOffset
25
20
  import androidx.compose.ui.unit.dp
21
+ import androidx.navigation.NavController
22
+ import androidx.navigation.compose.currentBackStackEntryAsState
26
23
  import vn.momo.kits.application.IsShowBaseLineDebug
27
24
  import vn.momo.kits.components.*
28
25
  import vn.momo.kits.const.*
29
26
  import vn.momo.kits.modifier.InternalApi
30
27
  import vn.momo.kits.modifier.conditional
31
- import vn.momo.kits.modifier.activeOpacityClickable
28
+ import vn.momo.kits.modifier.noFeedbackClickable
32
29
  import vn.momo.kits.platform.getScreenDimensions
33
30
 
31
+ internal val floatingButtonWidth = 75.dp
34
32
  @InternalApi
35
33
  const val BOTTOM_TAB_BAR_HEIGHT = 64
36
34
 
@@ -38,24 +36,28 @@ const val BOTTOM_TAB_BAR_HEIGHT = 64
38
36
  internal fun BottomTabBar(
39
37
  items: List<BottomTabItem>,
40
38
  floatingButton: BottomTabFloatingButton? = null,
41
- selectedIndex: Int,
39
+ navController: NavController,
42
40
  onTabSelected: (Int) -> Unit,
43
41
  ) {
42
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
43
+ val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
44
44
 
45
45
  val screenWidth = getScreenDimensions().width.dp
46
46
  val mid = items.size / 2
47
47
 
48
- val floating = floatingButton?.takeIf { items.size % 2 == 0 }
49
- val slotWidth = screenWidth / (items.size + if (floating != null) 1 else 0)
50
- fun slotOf(index: Int) = if (floating != null && index >= mid) index + 1 else index
51
-
52
- var indicatorSlot by rememberSaveable { mutableIntStateOf(0) }
53
- val indicatorX = remember { Animatable(indicatorSlot.toFloat()) }
54
- LaunchedEffect(selectedIndex) {
55
- val target = slotOf(selectedIndex)
56
- indicatorSlot = target
57
- indicatorX.animateTo(target.toFloat(), tabTimingSpec)
48
+ val tabWidth = if (floatingButton != null) {
49
+ (screenWidth - floatingButtonWidth) / items.size
50
+ } else {
51
+ screenWidth / items.size
58
52
  }
53
+ val adjustSize = if(floatingButton != null && selectedIndex >= mid) floatingButtonWidth else 0.dp
54
+ val targetOffset = tabWidth * selectedIndex + tabWidth / 2 - screenWidth / 2 + adjustSize
55
+
56
+ val indicatorOffsetX by animateDpAsState(
57
+ targetValue = targetOffset,
58
+ animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
59
+ label = "IndicatorX"
60
+ )
59
61
 
60
62
  Box(
61
63
  contentAlignment = Alignment.BottomCenter
@@ -70,17 +72,17 @@ internal fun BottomTabBar(
70
72
  },
71
73
  horizontalArrangement = Arrangement.SpaceAround,
72
74
  ) {
73
- if (floating == null) {
75
+ if (floatingButton == null) {
74
76
  RenderTabBarItem(items, 0, items.size, selectedIndex, onTabSelected)
75
77
  } else {
76
78
  RenderTabBarItem(items, 0, mid, selectedIndex, onTabSelected)
77
- Spacer(modifier = Modifier.width(slotWidth))
79
+ Spacer(modifier = Modifier.width(floatingButtonWidth).padding(horizontal = Spacing.XXS))
78
80
  RenderTabBarItem(items, mid, items.size, selectedIndex, onTabSelected)
79
81
  }
80
82
  }
81
83
 
82
84
  Box(modifier = Modifier
83
- .offset { IntOffset(((slotWidth * (indicatorX.value + 0.5f)) - screenWidth / 2).roundToPx(), (-BOTTOM_TAB_BAR_HEIGHT + 2).dp.roundToPx()) }
85
+ .offset(x = indicatorOffsetX, y = (-BOTTOM_TAB_BAR_HEIGHT + 2).dp)
84
86
  .height(2.dp)
85
87
  .width(44.dp)
86
88
  .background(
@@ -99,7 +101,7 @@ internal fun BottomTabBar(
99
101
  )
100
102
  )
101
103
  )
102
- floating?.let { FloatingButton(it, slotWidth) }
104
+ floatingButton?.let { FloatingButton(it) }
103
105
  }
104
106
  }
105
107
 
@@ -124,10 +126,17 @@ internal fun RowScope.RenderTabBarItem(
124
126
 
125
127
  @Composable
126
128
  internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
127
- // RN parity: "0" hides the badge, "" renders a dot.
128
- val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
129
- val isNumber = badgeLabel?.toIntOrNull() != null
130
- val showDot = badgeLabel?.isEmpty() ?: item.showDot
129
+ fun isNumber(label: String): Boolean {
130
+ val numberRegex = "^\\d+$".toRegex()
131
+ return numberRegex.matches(label)
132
+ }
133
+ fun formatLabel(label: String): String? {
134
+ if (isNumber(label) && label.toInt() == 0) {
135
+ return null
136
+ }
137
+ return label
138
+ }
139
+ val badgeLabel = item.badgeLabel?.let { formatLabel(it) }
131
140
 
132
141
  Box(modifier = Modifier
133
142
  .fillMaxSize()
@@ -135,7 +144,7 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
135
144
  .conditional(IsShowBaseLineDebug) {
136
145
  border(1.dp, Colors.blue_03)
137
146
  }
138
- .activeOpacityClickable {
147
+ .noFeedbackClickable {
139
148
  onClick()
140
149
  },
141
150
  contentAlignment = Alignment.BottomStart
@@ -143,23 +152,18 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
143
152
  Column(
144
153
  modifier = Modifier
145
154
  .fillMaxSize()
146
- .padding(horizontal = Spacing.S, vertical = Spacing.S),
155
+ .padding(horizontal = Spacing.S, vertical = Spacing.S)
156
+ .noFeedbackClickable {
157
+ onClick()
158
+ },
147
159
  horizontalAlignment = Alignment.CenterHorizontally,
148
160
  verticalArrangement = Arrangement.Center
149
161
  ) {
150
- Box(modifier = Modifier.padding(Spacing.XS)) {
151
- Icon(
152
- source = item.icon,
153
- size = 28.dp,
154
- color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
155
- // Anchored to the icon (not the tab cell) so it stays put whatever the tab width; matchParentSize + unbounded wrap keeps it from resizing the icon slot.
156
- val badgeAnchor = Modifier.matchParentSize().wrapContentSize(Alignment.TopEnd, unbounded = true)
157
- if (showDot) {
158
- BadgeDot(size = DotSize.Small, modifier = badgeAnchor.offset(x = 4.dp, y = (-4).dp))
159
- } else if (!badgeLabel.isNullOrEmpty()) {
160
- Badge(badgeLabel, modifier = badgeAnchor.offset(x = if (isNumber) 8.dp else 12.dp, y = (-8).dp))
161
- }
162
- }
162
+ Icon(
163
+ source = item.icon,
164
+ size = 28.dp,
165
+ modifier = Modifier.padding(Spacing.XS),
166
+ color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
163
167
  Text(
164
168
  text = item.label,
165
169
  color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
@@ -168,19 +172,28 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
168
172
  overflow = TextOverflow.Ellipsis
169
173
  )
170
174
  }
175
+ if (badgeLabel != null) {
176
+ if (badgeLabel.isEmpty())
177
+ BadgeDot(
178
+ size = DotSize.Small,
179
+ modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
180
+ )
181
+ else
182
+ Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
183
+ }
171
184
  }
172
185
  }
173
186
 
174
187
  @Composable
175
- internal fun FloatingButton(data: BottomTabFloatingButton, width: Dp) {
188
+ internal fun FloatingButton(data: BottomTabFloatingButton) {
176
189
  Column(
177
190
  modifier = Modifier
178
- .width(width)
191
+ .width(floatingButtonWidth)
179
192
  .padding(horizontal = Spacing.XXS, vertical = Spacing.S)
180
193
  .conditional(IsShowBaseLineDebug) {
181
194
  border(1.dp, Colors.blue_03)
182
195
  }
183
- .activeOpacityClickable {
196
+ .noFeedbackClickable {
184
197
  data.onPress()
185
198
  },
186
199
  verticalArrangement = Arrangement.Bottom,
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.167.1-beta.20
21
+ version=0.167.1-beta.21
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -204,6 +204,7 @@ public struct BadgeRibbon: View {
204
204
  /// rotation) — the label gets the same scale applied a second time so it stays right-reading
205
205
  /// while only the ribbon shape flips.
206
206
  public struct RoundedBadgeRibbon: View {
207
+ @Environment(\.appTheme) private var theme
207
208
  @Environment(\.momoFontScale) private var fontScale
208
209
  let text: String
209
210
  let position: RibbonPosition
@@ -224,7 +225,7 @@ public struct RoundedBadgeRibbon: View {
224
225
 
225
226
  private var measuredTextSize: CGSize {
226
227
  let fontSize = fontScale(TypographyStyle.labelXsMedium.fontSize)
227
- let font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
228
+ let font = momoUIFont(size: fontSize, weight: .medium, family: theme.font)
228
229
  return text.size(withAttributes: [.font: font])
229
230
  }
230
231
 
@@ -181,7 +181,7 @@ public struct Input: View {
181
181
  handleFocusChange(focused)
182
182
  })
183
183
  .keyboardType(keyboardType)
184
- .font(fontWeight == .bold ? .action_s_bold : .body_default_regular)
184
+ .kitFont(fontWeight == .bold ? .actionSBold : .bodyDefaultRegular)
185
185
  .ignoreBoldTextSetting()
186
186
  .foregroundColor(getTextColor())
187
187
  .disabled(disabled || readOnly)
@@ -349,7 +349,7 @@ private struct SecureInputField: UIViewRepresentable {
349
349
  textField.isSecureTextEntry = true
350
350
  textField.delegate = context.coordinator
351
351
  textField.keyboardType = keyboardType
352
- textField.font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight)
352
+ textField.font = momoUIFont(size: fontSize, weight: fontWeight, family: context.environment.appTheme.font)
353
353
  textField.textColor = textColor
354
354
  textField.isEnabled = !isDisabled
355
355
  textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -105,7 +105,7 @@ public struct InputPhoneNumber: View {
105
105
  handleFocusChange(focused)
106
106
  })
107
107
  .keyboardType(.numberPad)
108
- .font(size == .small ? .header_s_semibold : .header_m_bold)
108
+ .kitFont(size == .small ? .headerSSemibold : .headerMBold)
109
109
  .ignoreBoldTextSetting()
110
110
  .foregroundColor(theme.colors.text.default)
111
111
  .applyCursorColor(theme.colors.primary)
@@ -104,7 +104,6 @@ public struct InputSearchProps {
104
104
 
105
105
  public struct InputSearch: View {
106
106
  @Environment(\.appTheme) private var theme
107
- @Environment(\.momoFontScale) private var fontScale
108
107
  @Binding var text: String
109
108
  var buttonText: String = ""
110
109
  var showButtonText: Bool = false
@@ -215,7 +214,7 @@ public struct InputSearch: View {
215
214
  } else {
216
215
  Text(placeholder)
217
216
  .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
218
- .font(.system(size: fontScale(14), weight: fontWeight))
217
+ .kitFont(size: 14, weight: fontWeight)
219
218
  .ignoreBoldTextSetting()
220
219
  }
221
220
  }
@@ -232,7 +231,7 @@ public struct InputSearch: View {
232
231
  }, onCommit: {})
233
232
  .disabled(disabled)
234
233
  .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.default)
235
- .font(.system(size: fontScale(14), weight: fontWeight))
234
+ .kitFont(size: 14, weight: fontWeight)
236
235
  .ignoreBoldTextSetting()
237
236
  .keyboardType(keyboardType)
238
237
  .applyCursorColor(theme.colors.primary)
@@ -39,9 +39,10 @@ struct UITextViewWrapper: UIViewRepresentable {
39
39
  uiView.tintColor = UIColor(cursorColor)
40
40
  // Read through `context.environment` rather than the global `scaleSize`: that is what makes
41
41
  // SwiftUI re-run updateUIView when the font scale changes.
42
- uiView.font = UIFont.systemFont(
43
- ofSize: context.environment.momoFontScale(16),
44
- weight: fontWeight == .bold ? .bold : .regular
42
+ uiView.font = momoUIFont(
43
+ size: context.environment.momoFontScale(16),
44
+ weight: fontWeight == .bold ? .bold : .regular,
45
+ family: context.environment.appTheme.font
45
46
  )
46
47
  uiView.isEditable = !disabled
47
48
  }
@@ -210,7 +211,7 @@ public struct InputTextArea: View {
210
211
  if text.isEmpty {
211
212
  Text(placeholder)
212
213
  .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
213
- .font(.system(size: 16, weight: fontWeight))
214
+ .kitFont(size: 16, weight: fontWeight)
214
215
  .padding(.horizontal, 16)
215
216
  .padding(.vertical, 12)
216
217
  }
@@ -315,7 +315,11 @@ private struct MoneyTextField: UIViewRepresentable {
315
315
  let field = UITextField()
316
316
  field.delegate = context.coordinator
317
317
  field.keyboardType = keyboardType
318
- field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
318
+ field.font = momoUIFont(
319
+ size: context.environment.momoFontScale(20),
320
+ weight: fontWeight,
321
+ family: context.environment.appTheme.font
322
+ )
319
323
  field.textColor = textColor
320
324
  field.isEnabled = !isDisabled
321
325
  field.tintColor = cursorColor
@@ -328,7 +332,11 @@ private struct MoneyTextField: UIViewRepresentable {
328
332
  func updateUIView(_ field: UITextField, context: Context) {
329
333
  context.coordinator.parent = self
330
334
  // Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
331
- field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
335
+ field.font = momoUIFont(
336
+ size: context.environment.momoFontScale(20),
337
+ weight: fontWeight,
338
+ family: context.environment.appTheme.font
339
+ )
332
340
  field.isEnabled = !isDisabled
333
341
  field.textColor = textColor
334
342
  field.tintColor = cursorColor
@@ -84,7 +84,6 @@ private struct StepperButton: View {
84
84
 
85
85
  private struct StepperNumberView: View {
86
86
  @Environment(\.appTheme) private var theme
87
- @Environment(\.momoFontScale) private var fontScale
88
87
 
89
88
  @Binding var text: String
90
89
  var size: StepperSize
@@ -106,7 +105,7 @@ private struct StepperNumberView: View {
106
105
  MomoText(text, typography: .descriptionDefaultRegular, color: textColor)
107
106
  }
108
107
  }
109
- .font(.system(size: fontScale(12)))
108
+ .kitFont(size: 12)
110
109
  .ignoreBoldTextSetting()
111
110
  .foregroundColor(textColor)
112
111
  .frame(minWidth: specs.numberMinWidth)
@@ -123,6 +123,54 @@ private func momoFont(size: CGFloat, weight: Font.Weight, family: String) -> Fon
123
123
  return .custom(name, fixedSize: size)
124
124
  }
125
125
 
126
+ /// UIKit counterpart of `momoFont`, for text that is not a `MomoText` (UITextField/UITextView, text measuring).
127
+ func momoUIFont(size: CGFloat, weight: UIFont.Weight, family: String) -> UIFont {
128
+ guard let name = momoFontName(family: family, weight: Font.Weight(weight)),
129
+ let font = UIFont(name: name, size: size) else {
130
+ return .systemFont(ofSize: size, weight: weight)
131
+ }
132
+ return font
133
+ }
134
+
135
+ private extension Font.Weight {
136
+ init(_ weight: UIFont.Weight) {
137
+ switch weight {
138
+ case .ultraLight: self = .ultraLight
139
+ case .thin: self = .thin
140
+ case .light: self = .light
141
+ case .medium: self = .medium
142
+ case .semibold: self = .semibold
143
+ case .bold: self = .bold
144
+ case .heavy: self = .heavy
145
+ case .black: self = .black
146
+ default: self = .regular
147
+ }
148
+ }
149
+ }
150
+
151
+ /// Gives input text the same family as `MomoText` (Compose's `getFontFamily`), e.g. a TextField
152
+ /// next to a `MomoText` placeholder. `size` is unscaled; the environment font scale is applied here.
153
+ private struct KitFontModifier: ViewModifier {
154
+ @Environment(\.appTheme) private var theme
155
+ @Environment(\.momoFontScale) private var fontScale
156
+ let size: CGFloat
157
+ let weight: Font.Weight
158
+
159
+ func body(content: Content) -> some View {
160
+ content.font(momoFont(size: fontScale(size), weight: weight, family: theme.font))
161
+ }
162
+ }
163
+
164
+ extension View {
165
+ func kitFont(_ typography: TypographyStyle) -> some View {
166
+ modifier(KitFontModifier(size: typography.fontSize, weight: typography.fontWeight))
167
+ }
168
+
169
+ func kitFont(size: CGFloat, weight: Font.Weight = .regular) -> some View {
170
+ modifier(KitFontModifier(size: size, weight: weight))
171
+ }
172
+ }
173
+
126
174
  public struct MomoText: View {
127
175
  @Environment(\.appTheme) private var theme
128
176
  @Environment(\.momoFontScale) private var fontScale
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.167.1-beta.20'
3
+ s.version = '0.167.1-beta.21'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.167.1-beta.20'
3
+ s.version = '0.167.1-beta.21'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
package/local.properties CHANGED
@@ -4,5 +4,5 @@
4
4
  # Location of the SDK. This is only used by Gradle.
5
5
  # For customization when using a Version Control System, please read the
6
6
  # header note.
7
- #Thu Jun 04 17:11:56 ICT 2026
8
- sdk.dir=/Users/open.design/Library/Android/sdk
7
+ #Tue Apr 08 11:13:31 ICT 2025
8
+ sdk.dir=/Users/sonnguyen/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.167.1-beta.20-debug",
3
+ "version": "0.167.1-beta.21-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},