@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.
- package/compose/build.gradle.kts +1 -9
- package/compose/build.gradle.kts.backup +0 -8
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +73 -90
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +59 -46
- package/gradle.properties +1 -1
- package/ios/Badge/BadgeRibbon.swift +2 -1
- package/ios/Input/Input.swift +2 -2
- package/ios/Input/InputPhoneNumber.swift +1 -1
- package/ios/Input/InputSearch.swift +2 -3
- package/ios/Input/InputTextArea.swift +5 -4
- package/ios/InputMoney/InputMoney.swift +10 -2
- package/ios/Stepper/Stepper.swift +1 -2
- package/ios/Typography/Text.swift +48 -0
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/local.properties +2 -2
- package/package.json +1 -1
package/compose/build.gradle.kts
CHANGED
|
@@ -40,7 +40,7 @@ kotlin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
cocoapods {
|
|
43
|
-
version = "0.167.1-beta.
|
|
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
|
-
}
|
package/compose/compose.podspec
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
25
|
-
import androidx.compose.
|
|
26
|
-
import androidx.compose.
|
|
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
|
|
74
|
-
val
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
val
|
|
92
|
-
val
|
|
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 ({
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 = !
|
|
165
|
-
if (backToInitialTab)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
188
|
+
internal fun FloatingButton(data: BottomTabFloatingButton) {
|
|
176
189
|
Column(
|
|
177
190
|
modifier = Modifier
|
|
178
|
-
.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
|
-
.
|
|
196
|
+
.noFeedbackClickable {
|
|
184
197
|
data.onPress()
|
|
185
198
|
},
|
|
186
199
|
verticalArrangement = Arrangement.Bottom,
|
package/gradle.properties
CHANGED
|
@@ -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 =
|
|
228
|
+
let font = momoUIFont(size: fontSize, weight: .medium, family: theme.font)
|
|
228
229
|
return text.size(withAttributes: [.font: font])
|
|
229
230
|
}
|
|
230
231
|
|
package/ios/Input/Input.swift
CHANGED
|
@@ -181,7 +181,7 @@ public struct Input: View {
|
|
|
181
181
|
handleFocusChange(focused)
|
|
182
182
|
})
|
|
183
183
|
.keyboardType(keyboardType)
|
|
184
|
-
.
|
|
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 =
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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 =
|
|
43
|
-
|
|
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
|
-
.
|
|
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 =
|
|
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 =
|
|
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
|
-
.
|
|
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
|
package/ios/native-kits.podspec
CHANGED
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
|
-
#
|
|
8
|
-
sdk.dir=/Users/
|
|
7
|
+
#Tue Apr 08 11:13:31 ICT 2025
|
|
8
|
+
sdk.dir=/Users/sonnguyen/Library/Android/sdk
|