@momo-kits/native-kits 0.164.1-beta.9-debug → 0.164.1-headerconfig.1-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/.claude/settings.local.json +7 -178
- package/CLAUDE.md +2 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +3 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +30 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +8 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +9 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +4 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +104 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +8 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +38 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +20 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +10 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +20 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +11 -27
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +4 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +19 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +41 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +75 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +5 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +5 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +6 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +8 -23
- package/gradle/libs.versions.toml +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Navigation/NavigatorManager.swift +0 -27
- package/ios/Colors+Radius+Spacing/Theme.swift +2 -2
- package/ios/Typography/KitDesignSystem.swift +17 -0
- package/ios/Typography/Text.swift +25 -1
- 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/publish.sh +25 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ComposeBackGesture.ios.kt +0 -65
|
@@ -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
|
-
|
|
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
|
-
|
|
330
|
+
.padding(top = Spacing.XS)
|
|
331
|
+
.height(CARD_TAB_HEIGHT),
|
|
313
332
|
) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
.
|
|
321
|
-
.
|
|
386
|
+
.width(itemWidth)
|
|
387
|
+
.fillMaxHeight()
|
|
322
388
|
.background(
|
|
323
|
-
color =
|
|
324
|
-
|
|
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(
|
|
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 =
|
|
397
|
+
TabLeadingIcon(tab = active, active = true, color = selectedColor)
|
|
336
398
|
Text(
|
|
337
|
-
text =
|
|
338
|
-
style =
|
|
339
|
-
color =
|
|
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(
|
|
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
|
-
|
|
82
|
-
val
|
|
83
|
-
val
|
|
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
|
-
.
|
|
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 = "
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
115
|
-
val
|
|
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(
|
|
150
|
+
?: hostFontFamily(defaultFile)
|
|
118
151
|
?: FontFamily.Default
|
|
119
152
|
}
|
|
120
153
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
interface HostBackTarget {
|
|
4
|
+
fun canPop(): Boolean
|
|
5
|
+
|
|
6
|
+
fun onBack()
|
|
7
|
+
|
|
8
|
+
fun backStarted()
|
|
9
|
+
fun backProgressed(progress: Float)
|
|
10
|
+
fun backCompleted()
|
|
11
|
+
fun backCancelled()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
object HostBackGestureBridge {
|
|
15
|
+
private val targets = mutableListOf<HostBackTarget>()
|
|
16
|
+
private var gestureTarget: HostBackTarget? = null
|
|
17
|
+
|
|
18
|
+
var shouldUseFloatingArrowBack: Boolean = false
|
|
19
|
+
|
|
20
|
+
fun checkCanPop(): Boolean = targets.isNotEmpty()
|
|
21
|
+
|
|
22
|
+
fun canPopInner(): Boolean = top()?.canPop() == true
|
|
23
|
+
|
|
24
|
+
fun pop() {
|
|
25
|
+
top()?.onBack()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun started() {
|
|
29
|
+
val target = top() ?: return
|
|
30
|
+
gestureTarget = target
|
|
31
|
+
target.backStarted()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fun progressed(progress: Float) {
|
|
35
|
+
gestureTarget?.backProgressed(progress.coerceIn(0f, 1f))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun completed() {
|
|
39
|
+
(gestureTarget ?: top())?.backCompleted()
|
|
40
|
+
gestureTarget = null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun cancelled() {
|
|
44
|
+
gestureTarget?.backCancelled()
|
|
45
|
+
gestureTarget = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun attach(target: HostBackTarget) {
|
|
49
|
+
targets += target
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun detach(target: HostBackTarget) {
|
|
53
|
+
targets -= target
|
|
54
|
+
if (gestureTarget === target) gestureTarget = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun top(): HostBackTarget? = targets.lastOrNull()
|
|
58
|
+
}
|
|
@@ -33,6 +33,7 @@ class Navigation(
|
|
|
33
33
|
tintColor: Color? = null,
|
|
34
34
|
footerComponent: (@Composable () -> Unit)? = null,
|
|
35
35
|
footerTransparent: Boolean? = null,
|
|
36
|
+
footerUseMargin: Boolean? = null,
|
|
36
37
|
floatingButtonProps: FloatingButtonProps? = null,
|
|
37
38
|
keyboardOptions: KeyboardOptions? = null
|
|
38
39
|
) {
|
|
@@ -50,6 +51,7 @@ class Navigation(
|
|
|
50
51
|
tintColor = tintColor ?: options.tintColor,
|
|
51
52
|
footerComponent = footerComponent ?: options.footerComponent,
|
|
52
53
|
footerTransparent = footerTransparent ?: options.footerTransparent,
|
|
54
|
+
footerUseMargin = footerUseMargin ?: options.footerUseMargin,
|
|
53
55
|
floatingButtonProps = floatingButtonProps ?: options.floatingButtonProps,
|
|
54
56
|
keyboardOptions = keyboardOptions ?: options.keyboardOptions,
|
|
55
57
|
)
|
|
@@ -86,6 +88,7 @@ data class NavigationOptions(
|
|
|
86
88
|
val tintColor: Color? = null,
|
|
87
89
|
val footerComponent: @Composable (() -> Unit)? = null,
|
|
88
90
|
val footerTransparent: Boolean = false,
|
|
91
|
+
val footerUseMargin: Boolean = true,
|
|
89
92
|
val floatingButtonProps: FloatingButtonProps? = null,
|
|
90
93
|
val keyboardOptions: KeyboardOptions = KeyboardOptions()
|
|
91
94
|
) {
|
|
@@ -19,7 +19,10 @@ import androidx.navigation.compose.rememberNavController
|
|
|
19
19
|
import androidx.navigation.toRoute
|
|
20
20
|
import kotlinx.coroutines.delay
|
|
21
21
|
import kotlinx.coroutines.flow.collectLatest
|
|
22
|
+
import vn.momo.kits.application.DesignSystemConfig
|
|
23
|
+
import vn.momo.kits.application.KitDesignSystemConfig
|
|
22
24
|
import vn.momo.kits.application.LocalContext
|
|
25
|
+
import vn.momo.kits.application.LocalDesignSystemConfig
|
|
23
26
|
import vn.momo.kits.application.LocalLocalize
|
|
24
27
|
import vn.momo.kits.application.Localize
|
|
25
28
|
import vn.momo.kits.application.MiniAppContext
|
|
@@ -39,13 +42,14 @@ import kotlin.time.Duration.Companion.milliseconds
|
|
|
39
42
|
fun NavigationContainer(
|
|
40
43
|
initialScreenName: String,
|
|
41
44
|
initialScreen: @Composable () -> Unit,
|
|
45
|
+
maxApi: IMaxApi,
|
|
42
46
|
options: NavigationOptions? = null,
|
|
43
47
|
initialTheme: Theme = defaultTheme,
|
|
44
48
|
miniAppContext: MiniAppContext? = null,
|
|
45
|
-
maxApi: IMaxApi? = null,
|
|
46
49
|
setNavigator: ((Navigator) -> Unit)? = null,
|
|
47
50
|
statusBarHeight: Dp? = null,
|
|
48
51
|
localize: Localize? = null,
|
|
52
|
+
designSystemConfig: DesignSystemConfig? = null,
|
|
49
53
|
) {
|
|
50
54
|
val navController = rememberNavController()
|
|
51
55
|
val registry = remember { DynamicScreenRegistry() }
|
|
@@ -58,6 +62,17 @@ fun NavigationContainer(
|
|
|
58
62
|
|
|
59
63
|
val stackTransition = remember { StackTransitionCoordinator() }
|
|
60
64
|
val theme = remember { mutableStateOf(initialTheme) }
|
|
65
|
+
val resolvedDesignSystemConfig =
|
|
66
|
+
designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
|
|
67
|
+
val headerBar = resolvedDesignSystemConfig?.headerBar
|
|
68
|
+
// RN parity: the theme's own header image wins; config headerBar is only the fallback.
|
|
69
|
+
val resolvedTheme = remember(theme.value, headerBar) {
|
|
70
|
+
if (theme.value.isHeaderImage() || headerBar.isNullOrEmpty()) {
|
|
71
|
+
theme.value
|
|
72
|
+
} else {
|
|
73
|
+
theme.value.copy(assets = theme.value.assets.copy(headerBackground = headerBar))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
61
76
|
val fallbackLocalize = remember { Localize() }
|
|
62
77
|
|
|
63
78
|
val resolvedLocalize = localize ?: fallbackLocalize
|
|
@@ -78,15 +93,16 @@ fun NavigationContainer(
|
|
|
78
93
|
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
79
94
|
|
|
80
95
|
ProvideOsFontScale {
|
|
81
|
-
ProvideNavigationEventDispatcherOwner {
|
|
96
|
+
ProvideNavigationEventDispatcherOwner(navigator) {
|
|
82
97
|
CompositionLocalProvider(
|
|
83
98
|
LocalNavigator provides navigator,
|
|
84
99
|
LocalDynamicScreenRegistry provides registry,
|
|
85
100
|
LocalMaxApi provides maxApi,
|
|
86
|
-
AppTheme provides
|
|
101
|
+
AppTheme provides resolvedTheme,
|
|
87
102
|
AppStatusBar provides statusBarHeight,
|
|
88
103
|
AppNavigationBar provides navigationBarHeight,
|
|
89
104
|
LocalContext provides mergedContext,
|
|
105
|
+
LocalDesignSystemConfig provides resolvedDesignSystemConfig,
|
|
90
106
|
LocalLocalize provides resolvedLocalize,
|
|
91
107
|
LocalStackTransition provides stackTransition,
|
|
92
108
|
) {
|
|
@@ -96,8 +112,6 @@ fun NavigationContainer(
|
|
|
96
112
|
|
|
97
113
|
SwipeBackHaptic(maxApi)
|
|
98
114
|
|
|
99
|
-
// Gesture pops never reach Navigator.dismissScreen(), so prune the registry from
|
|
100
|
-
// the back stack itself. collectLatest lets a rapid re-push cancel a pending prune.
|
|
101
115
|
LaunchedEffect(navController, registry) {
|
|
102
116
|
navController.currentBackStack.collectLatest { entries ->
|
|
103
117
|
delay(STACK_TRANSITION_DURATION.milliseconds)
|
|
@@ -110,10 +124,6 @@ fun NavigationContainer(
|
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
// The transitions must live on the NavHost call site, not on the destinations:
|
|
114
|
-
// NavHost.uikit computes isDefaultTransition from these arguments alone, and while
|
|
115
|
-
// it is true the library paints its own hard-coded black iOS blackout that would
|
|
116
|
-
// sit over the pink scrim.
|
|
117
127
|
NavHost(
|
|
118
128
|
navController,
|
|
119
129
|
startDestination = startDestination,
|
|
@@ -163,8 +173,7 @@ fun NavigationContainer(
|
|
|
163
173
|
id = route.id,
|
|
164
174
|
name = screen.name,
|
|
165
175
|
content = screen.content,
|
|
166
|
-
navigationOptions = screen.options
|
|
167
|
-
isDialogRoute = true
|
|
176
|
+
navigationOptions = screen.options
|
|
168
177
|
)
|
|
169
178
|
}
|
|
170
179
|
}
|
|
@@ -4,15 +4,11 @@ import androidx.compose.foundation.layout.Box
|
|
|
4
4
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
5
5
|
import androidx.compose.runtime.Composable
|
|
6
6
|
import androidx.compose.runtime.MutableState
|
|
7
|
-
import androidx.compose.runtime.collectAsState
|
|
8
|
-
import androidx.compose.runtime.getValue
|
|
9
7
|
import androidx.compose.runtime.mutableStateOf
|
|
10
8
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
11
9
|
import androidx.compose.ui.Alignment
|
|
12
10
|
import androidx.compose.ui.Modifier
|
|
13
|
-
import androidx.navigation.NavBackStackEntry
|
|
14
11
|
import androidx.navigation.NavController
|
|
15
|
-
import androidx.navigation.NavGraph
|
|
16
12
|
import androidx.navigation.toRoute
|
|
17
13
|
import kotlinx.coroutines.CoroutineScope
|
|
18
14
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -35,6 +31,9 @@ class Navigator(
|
|
|
35
31
|
) {
|
|
36
32
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
37
33
|
|
|
34
|
+
// currentScreen() reads the root back stack, so it never sees the nested tab NavHost.
|
|
35
|
+
internal var tabBackHandler: (() -> Unit)? = null
|
|
36
|
+
|
|
38
37
|
private fun currentScreen(): DynamicScreen? {
|
|
39
38
|
val entry = navController.currentBackStackEntry ?: return null
|
|
40
39
|
val id = runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
|
|
@@ -56,6 +55,10 @@ class Navigator(
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
fun onBackSafe(callBack: (() -> Unit)? = null) {
|
|
58
|
+
tabBackHandler?.let {
|
|
59
|
+
it()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
59
62
|
val currentScreen = currentScreen()
|
|
60
63
|
val options = currentScreen?.options
|
|
61
64
|
if (options?.onBackHandler != null) {
|
|
@@ -83,21 +86,9 @@ class Navigator(
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// The swipe-back opt-out predicate in StackScreen needs this: the root screen must keep custom
|
|
86
|
-
// back handling so it still reaches maxApi.dismiss.
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
@Composable
|
|
90
|
-
internal fun rememberCanPop(): Boolean {
|
|
91
|
-
val backStack by navController.currentBackStack.collectAsState()
|
|
92
|
-
return backStack.hasPoppableEntry()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Non-composable twin of [rememberCanPop] for the iOS host: its own back check is mount-based
|
|
97
|
-
* and so answers `true` even on the mini-app root, where a swipe must dismiss rather than pop.
|
|
98
|
-
* Reads the StateFlow value directly — correct here precisely because nothing recomposes on it.
|
|
99
|
-
*/
|
|
100
|
-
fun canPop(): Boolean = navController.currentBackStack.value.hasPoppableEntry()
|
|
89
|
+
// back handling so it still reaches maxApi.dismiss.
|
|
90
|
+
// Public: the iOS host asks this to decide whether the kit owns the swipe on this screen.
|
|
91
|
+
fun canPop(): Boolean = navController.previousBackStackEntry != null
|
|
101
92
|
|
|
102
93
|
fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
|
|
103
94
|
scope.launch {
|
|
@@ -196,10 +187,6 @@ class Navigator(
|
|
|
196
187
|
}
|
|
197
188
|
}
|
|
198
189
|
|
|
199
|
-
// The NavGraph itself sits in the back stack as an entry; only real destinations can be popped.
|
|
200
|
-
private fun List<NavBackStackEntry>.hasPoppableEntry(): Boolean =
|
|
201
|
-
count { it.destination !is NavGraph } > 1
|
|
202
|
-
|
|
203
190
|
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
|
204
191
|
error("No NavigationStack provided")
|
|
205
192
|
}
|
|
@@ -93,7 +93,6 @@ internal fun StackScreen(
|
|
|
93
93
|
id: Int = -1,
|
|
94
94
|
name: String = "",
|
|
95
95
|
bottomTabIndex: Int = -1,
|
|
96
|
-
isDialogRoute: Boolean = false,
|
|
97
96
|
onBackHandler: (() -> Unit)? = null,
|
|
98
97
|
) {
|
|
99
98
|
val navigator = LocalNavigator.current
|
|
@@ -170,29 +169,13 @@ internal fun StackScreen(
|
|
|
170
169
|
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
// whenever an earlier term is true.
|
|
175
|
-
val canPop = navigator.rememberCanPop()
|
|
172
|
+
val canPop = navigator.canPop()
|
|
176
173
|
|
|
177
|
-
// Port of RN's `hasCustomBackHandling`: this handler is registered inside the NavHost
|
|
178
|
-
// destination, so while it is enabled it beats NavHost's own predictive-back handler and
|
|
179
|
-
// swallows the swipe. Enable it only when a plain pop is the wrong answer.
|
|
180
|
-
//
|
|
181
|
-
// A presented route is one of those cases: NavHost arms predictive back on back-stack size
|
|
182
|
-
// alone, so a horizontal edge drag over a dialog would seek its *vertical* slide.
|
|
183
174
|
val hasCustomBackHandling = !SwipeBackConfig.enabled ||
|
|
184
|
-
isBottomTabChild ||
|
|
185
|
-
isDialogRoute ||
|
|
186
|
-
options.onBackHandler != null ||
|
|
187
|
-
options.headerBackProps.preventBack != null ||
|
|
188
175
|
OverplayComponentRegistry.isOverplayShowing() ||
|
|
189
176
|
!canPop
|
|
190
177
|
|
|
191
|
-
|
|
192
|
-
// the whole mini-app instead of returning to tab 0.
|
|
193
|
-
val onBack = onBackHandler ?: { navigator.onBackSafe() }
|
|
194
|
-
|
|
195
|
-
BackHandler(hasCustomBackHandling, onBack)
|
|
178
|
+
BackHandler(hasCustomBackHandling) { navigator.onBackSafe() }
|
|
196
179
|
|
|
197
180
|
CompositionLocalProvider(
|
|
198
181
|
StackScreenScrollableState provides options.scrollData.scrollState,
|
|
@@ -321,7 +304,10 @@ internal fun ScreenContent(content: @Composable () -> Unit) {
|
|
|
321
304
|
if (options.headerType is HeaderType.Animated) {
|
|
322
305
|
val animatedHeader = options.headerType
|
|
323
306
|
Box {
|
|
324
|
-
|
|
307
|
+
val bannerModifier = animatedHeader.height
|
|
308
|
+
?.let { Modifier.fillMaxWidth().height(it) }
|
|
309
|
+
?: Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)
|
|
310
|
+
Box(bannerModifier) {
|
|
325
311
|
animatedHeader.composable.invoke(scrollState.value)
|
|
326
312
|
}
|
|
327
313
|
Box(Modifier.padding(top = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
|
|
@@ -342,7 +328,8 @@ internal fun FooterContent() {
|
|
|
342
328
|
Footer(
|
|
343
329
|
footerComponent = options.footerComponent,
|
|
344
330
|
bottomPadding = bottomPadding,
|
|
345
|
-
transparent = options.footerTransparent
|
|
331
|
+
transparent = options.footerTransparent,
|
|
332
|
+
useMargin = options.footerUseMargin
|
|
346
333
|
)
|
|
347
334
|
}
|
|
348
335
|
}
|
|
@@ -367,7 +354,12 @@ internal fun OverplayView(isBottomTabChild: Boolean, id: Int) {
|
|
|
367
354
|
}
|
|
368
355
|
|
|
369
356
|
@Composable
|
|
370
|
-
fun Footer(
|
|
357
|
+
fun Footer(
|
|
358
|
+
footerComponent: @Composable (() -> Unit)?,
|
|
359
|
+
bottomPadding: Dp,
|
|
360
|
+
transparent: Boolean = false,
|
|
361
|
+
useMargin: Boolean = true,
|
|
362
|
+
) {
|
|
371
363
|
if (footerComponent == null) return
|
|
372
364
|
|
|
373
365
|
val footerHeightPx = LocalFooterHeightPx.current
|
|
@@ -390,7 +382,12 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp, transp
|
|
|
390
382
|
.conditional(IsShowBaseLineDebug) {
|
|
391
383
|
border(1.dp, Colors.blue_03)
|
|
392
384
|
}
|
|
393
|
-
.padding(
|
|
385
|
+
.padding(
|
|
386
|
+
top = if (useMargin) Spacing.S else 0.dp,
|
|
387
|
+
start = if (useMargin) Spacing.M else 0.dp,
|
|
388
|
+
end = if (useMargin) Spacing.M else 0.dp,
|
|
389
|
+
bottom = bottomPadding + if (useMargin) Spacing.S else 0.dp,
|
|
390
|
+
)
|
|
394
391
|
) {
|
|
395
392
|
footerComponent.invoke()
|
|
396
393
|
}
|