@momo-kits/native-kits 0.113.2-beta.5 → 0.113.3-beta.2
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/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +6 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +27 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +36 -32
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +5 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +50 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +1 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +12 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +23 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +11 -20
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +1 -1
- package/ios/Image/Image.swift +1 -1
- package/ios/Popup/PopupDisplay.swift +13 -6
- package/package.json +1 -1
|
@@ -3,12 +3,9 @@ package vn.momo.kits.platform
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.res.Resources
|
|
5
5
|
import android.graphics.BlurMaskFilter
|
|
6
|
-
import androidx.compose.foundation.layout.WindowInsets
|
|
7
|
-
import androidx.compose.foundation.layout.statusBars
|
|
8
6
|
import androidx.compose.runtime.Composable
|
|
9
7
|
import androidx.compose.ui.graphics.NativePaint
|
|
10
8
|
import androidx.compose.ui.platform.LocalConfiguration
|
|
11
|
-
import androidx.compose.ui.platform.LocalDensity
|
|
12
9
|
import androidx.compose.ui.unit.Dp
|
|
13
10
|
import androidx.compose.ui.unit.dp
|
|
14
11
|
|
|
@@ -31,6 +28,10 @@ actual fun NativePaint.setMaskFilter(blurRadius: Float) {
|
|
|
31
28
|
@SuppressLint("InternalInsetResource")
|
|
32
29
|
@Composable
|
|
33
30
|
actual fun getStatusBarHeight(): Dp {
|
|
34
|
-
val
|
|
35
|
-
|
|
31
|
+
val resourceId = Resources.getSystem().getIdentifier(
|
|
32
|
+
"status_bar_height",
|
|
33
|
+
"dimen",
|
|
34
|
+
"android"
|
|
35
|
+
)
|
|
36
|
+
return if(resourceId > 0) Resources.getSystem().getDimensionPixelSize(resourceId).dp else 0.dp
|
|
36
37
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package vn.momo.kits.application
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.Box
|
|
5
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
6
|
+
import androidx.compose.foundation.layout.height
|
|
7
|
+
import androidx.compose.foundation.layout.offset
|
|
8
|
+
import androidx.compose.foundation.layout.padding
|
|
9
|
+
import androidx.compose.foundation.layout.width
|
|
10
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
11
|
+
import androidx.compose.runtime.Composable
|
|
12
|
+
import androidx.compose.ui.Modifier
|
|
13
|
+
import androidx.compose.ui.graphics.Color
|
|
14
|
+
import androidx.compose.ui.unit.dp
|
|
15
|
+
import androidx.compose.ui.zIndex
|
|
16
|
+
import vn.momo.kits.components.InputSearch
|
|
17
|
+
import vn.momo.kits.components.InputSearchProps
|
|
18
|
+
import vn.momo.kits.const.Radius
|
|
19
|
+
import vn.momo.kits.const.Spacing
|
|
20
|
+
|
|
21
|
+
@Composable
|
|
22
|
+
fun AnimationSearchInput(
|
|
23
|
+
animations: HeaderAnimations,
|
|
24
|
+
inputSearchProps: InputSearchProps
|
|
25
|
+
) {
|
|
26
|
+
Box(
|
|
27
|
+
modifier = Modifier
|
|
28
|
+
.fillMaxWidth()
|
|
29
|
+
.offset(
|
|
30
|
+
x = animations.translateX.dp,
|
|
31
|
+
y = animations.translateY.dp
|
|
32
|
+
)
|
|
33
|
+
) {
|
|
34
|
+
Box(
|
|
35
|
+
modifier = Modifier
|
|
36
|
+
.height(52.dp)
|
|
37
|
+
.padding(vertical = Spacing.S)
|
|
38
|
+
.zIndex(1f)
|
|
39
|
+
) {
|
|
40
|
+
Box(
|
|
41
|
+
modifier = Modifier
|
|
42
|
+
.width(animations.width.dp)
|
|
43
|
+
.background(
|
|
44
|
+
color = animations.backgroundSearch,
|
|
45
|
+
shape = RoundedCornerShape(Radius.XL)
|
|
46
|
+
)
|
|
47
|
+
) {
|
|
48
|
+
InputSearch(
|
|
49
|
+
inputSearchProps = inputSearchProps.copy(
|
|
50
|
+
showButtonText = false,
|
|
51
|
+
backgroundColor = Color.Transparent
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package vn.momo.kits.application
|
|
2
2
|
|
|
3
|
-
import androidx.compose.animation.animateColorAsState
|
|
4
3
|
import androidx.compose.foundation.background
|
|
5
4
|
import androidx.compose.foundation.border
|
|
6
5
|
import androidx.compose.foundation.clickable
|
|
@@ -24,10 +23,10 @@ import androidx.compose.runtime.setValue
|
|
|
24
23
|
import androidx.compose.runtime.snapshotFlow
|
|
25
24
|
import androidx.compose.ui.Alignment
|
|
26
25
|
import androidx.compose.ui.Modifier
|
|
27
|
-
import androidx.compose.ui.graphics.Color
|
|
28
26
|
import androidx.compose.ui.graphics.graphicsLayer
|
|
29
27
|
import androidx.compose.ui.semantics.semantics
|
|
30
28
|
import androidx.compose.ui.semantics.testTag
|
|
29
|
+
import androidx.compose.ui.unit.Dp
|
|
31
30
|
import androidx.compose.ui.unit.dp
|
|
32
31
|
import androidx.compose.ui.zIndex
|
|
33
32
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
@@ -42,15 +41,6 @@ import vn.momo.kits.const.Spacing
|
|
|
42
41
|
import vn.momo.kits.modifier.setAutomationId
|
|
43
42
|
import vn.momo.kits.platform.getStatusBarHeight
|
|
44
43
|
|
|
45
|
-
enum class TitlePosition {
|
|
46
|
-
LEFT, CENTER,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
data class AnimatedHeader(
|
|
50
|
-
val type: HeaderType = HeaderType.DEFAULT,
|
|
51
|
-
val composable: @Composable (scrollState: Int) -> Unit = {}
|
|
52
|
-
)
|
|
53
|
-
|
|
54
44
|
@Composable
|
|
55
45
|
fun Header(
|
|
56
46
|
headerType: HeaderType = HeaderType.DEFAULT,
|
|
@@ -58,16 +48,16 @@ fun Header(
|
|
|
58
48
|
title: String,
|
|
59
49
|
headerRight: @Composable (() -> Unit)? = null,
|
|
60
50
|
goBack: (() -> Unit)? = null,
|
|
61
|
-
opacity: Float
|
|
51
|
+
opacity: Float = 1f,
|
|
62
52
|
animatedHeader: AnimatedHeader? = null,
|
|
63
53
|
scrollState: Int = 0,
|
|
64
54
|
inputSearchProps: InputSearchProps? = null,
|
|
55
|
+
headerRightWidth: Dp = 0.dp,
|
|
65
56
|
useAnimationSearch: Boolean = false
|
|
66
57
|
) {
|
|
67
58
|
val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
|
|
68
59
|
val tintIconColor = AppTheme.current.colors.text.default
|
|
69
60
|
val backgroundButton = Colors.black_01.copy(alpha = 0.6f)
|
|
70
|
-
val miniAppContext = ApplicationContext.current
|
|
71
61
|
|
|
72
62
|
var colorFraction by remember { mutableStateOf(0f) }
|
|
73
63
|
|
|
@@ -77,15 +67,22 @@ fun Header(
|
|
|
77
67
|
.distinctUntilChanged()
|
|
78
68
|
.collect { fraction -> colorFraction = fraction }
|
|
79
69
|
}
|
|
70
|
+
val animations = useHeaderSearchAnimation(
|
|
71
|
+
opacityAni = opacity,
|
|
72
|
+
scrollState = scrollState,
|
|
73
|
+
headerRightWidth = headerRightWidth,
|
|
74
|
+
isBack = goBack != null
|
|
75
|
+
)
|
|
80
76
|
|
|
81
|
-
val backgroundSearch
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
colorFraction
|
|
86
|
-
)
|
|
77
|
+
val backgroundSearch = animateColor(
|
|
78
|
+
Colors.black_01,
|
|
79
|
+
AppTheme.current.colors.background.default,
|
|
80
|
+
colorFraction
|
|
87
81
|
)
|
|
88
82
|
|
|
83
|
+
val searchAnimationEnable =
|
|
84
|
+
inputSearchProps != null && useAnimationSearch && headerType == HeaderType.EXTENDED
|
|
85
|
+
|
|
89
86
|
if (headerType != HeaderType.NONE) {
|
|
90
87
|
BoxWithConstraints(
|
|
91
88
|
Modifier.height(statusBarHeight + 52.dp)
|
|
@@ -132,9 +129,9 @@ fun Header(
|
|
|
132
129
|
}
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
if (
|
|
132
|
+
if (useAnimationSearch || inputSearchProps == null) {
|
|
136
133
|
Box(Modifier.graphicsLayer {
|
|
137
|
-
alpha = if (
|
|
134
|
+
alpha = if (animatedHeader !== null) opacity else 1f
|
|
138
135
|
}) {
|
|
139
136
|
headerRight?.invoke()
|
|
140
137
|
}
|
|
@@ -156,11 +153,7 @@ fun Header(
|
|
|
156
153
|
} else {
|
|
157
154
|
Box(
|
|
158
155
|
Modifier.weight(1f).graphicsLayer {
|
|
159
|
-
alpha = if (
|
|
160
|
-
1f - (opacity ?: 1f)
|
|
161
|
-
} else {
|
|
162
|
-
if (animatedHeader !== null) opacity ?: 1f else 1f
|
|
163
|
-
}
|
|
156
|
+
alpha = if (searchAnimationEnable) 1f - (opacity) else 1f
|
|
164
157
|
},
|
|
165
158
|
contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
|
|
166
159
|
) {
|
|
@@ -174,16 +167,13 @@ fun Header(
|
|
|
174
167
|
}
|
|
175
168
|
}
|
|
176
169
|
}
|
|
170
|
+
|
|
171
|
+
if (searchAnimationEnable) {
|
|
172
|
+
AnimationSearchInput(
|
|
173
|
+
animations = animations,
|
|
174
|
+
inputSearchProps = inputSearchProps!!
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
fun animateColor(start: Color, stop: Color, fraction: Float): Color {
|
|
183
|
-
return Color(
|
|
184
|
-
red = start.red + fraction * (stop.red - start.red),
|
|
185
|
-
green = start.green + fraction * (stop.green - start.green),
|
|
186
|
-
blue = start.blue + fraction * (stop.blue - start.blue),
|
|
187
|
-
alpha = start.alpha + fraction * (stop.alpha - start.alpha)
|
|
188
|
-
)
|
|
189
|
-
}
|
|
179
|
+
}
|
|
@@ -1,66 +1,70 @@
|
|
|
1
1
|
package vn.momo.kits.application
|
|
2
2
|
|
|
3
|
+
import androidx.compose.animation.animateColorAsState
|
|
4
|
+
import androidx.compose.animation.core.animateFloatAsState
|
|
3
5
|
import androidx.compose.foundation.layout.Box
|
|
4
6
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
5
7
|
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.runtime.getValue
|
|
6
9
|
import androidx.compose.ui.Modifier
|
|
7
10
|
import androidx.compose.ui.graphics.Color
|
|
8
11
|
import androidx.compose.ui.unit.Dp
|
|
9
12
|
import androidx.compose.ui.unit.dp
|
|
10
|
-
import vn.momo.kits.components.InputSearchProps
|
|
11
13
|
import vn.momo.kits.const.AppStatusBar
|
|
12
14
|
import vn.momo.kits.const.AppTheme
|
|
13
|
-
import vn.momo.kits.platform.getScreenDimensions
|
|
14
15
|
import vn.momo.kits.platform.getStatusBarHeight
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
enum class TitlePosition {
|
|
18
|
+
LEFT, CENTER,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
data class AnimatedHeader(
|
|
22
|
+
val type: HeaderType = HeaderType.DEFAULT,
|
|
23
|
+
val composable: @Composable (scrollState: Int) -> Unit = {}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
fun animateColor(start: Color, stop: Color, fraction: Float): Color {
|
|
27
|
+
return Color(
|
|
28
|
+
red = start.red + fraction * (stop.red - start.red),
|
|
29
|
+
green = start.green + fraction * (stop.green - start.green),
|
|
30
|
+
blue = start.blue + fraction * (stop.blue - start.blue),
|
|
31
|
+
alpha = start.alpha + fraction * (stop.alpha - start.alpha)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
18
34
|
|
|
19
35
|
@Composable
|
|
20
36
|
fun HeaderBackground(
|
|
21
|
-
isBack: Boolean = true,
|
|
22
37
|
headerType: HeaderType = HeaderType.DEFAULT,
|
|
23
38
|
scrollState: Int,
|
|
24
|
-
inputSearchProps: InputSearchProps? = null,
|
|
25
|
-
headerRightWidth: Dp = 0.dp,
|
|
26
|
-
useAnimationSearch: Boolean = false
|
|
27
39
|
) {
|
|
28
40
|
val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
|
|
29
|
-
val screenWidth = getScreenDimensions().width
|
|
30
|
-
val leftPosition = if (isBack) (BACK_WIDTH + 20).dp else 20.dp
|
|
31
|
-
val searchWidth = screenWidth - (SCREEN_PADDING * 2)
|
|
32
41
|
|
|
33
|
-
val
|
|
34
|
-
|
|
35
|
-
statusBarHeight = statusBarHeight,
|
|
36
|
-
leftPosition = leftPosition,
|
|
37
|
-
searchWidth = searchWidth,
|
|
38
|
-
headerRightWidth = headerRightWidth
|
|
42
|
+
val opacityAni by animateFloatAsState(
|
|
43
|
+
targetValue = (1 - (scrollState / 52f)).coerceIn(0f, 1f),
|
|
39
44
|
)
|
|
40
45
|
|
|
41
|
-
val backgroundColor
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
val backgroundColor by animateColorAsState(
|
|
47
|
+
targetValue = animateColor(
|
|
48
|
+
Color.Transparent,
|
|
49
|
+
AppTheme.current.colors.background.surface,
|
|
50
|
+
opacityAni
|
|
51
|
+
),
|
|
52
|
+
)
|
|
46
53
|
|
|
47
54
|
when (headerType) {
|
|
48
55
|
HeaderType.DEFAULT -> {
|
|
49
|
-
val height = statusBarHeight + 52.dp
|
|
50
56
|
HeaderDefault(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
backgroundColor = backgroundColor
|
|
57
|
+
opacityAni = opacityAni,
|
|
58
|
+
statusBarHeight = statusBarHeight,
|
|
54
59
|
)
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
HeaderType.EXTENDED -> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
63
|
+
HeaderExtended(
|
|
64
|
+
opacityAni = opacityAni,
|
|
65
|
+
statusBarHeight = statusBarHeight,
|
|
66
|
+
backgroundColor = backgroundColor,
|
|
67
|
+
)
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
else -> {
|
|
@@ -21,13 +21,13 @@ import vn.momo.kits.utils.bottomBorder
|
|
|
21
21
|
|
|
22
22
|
@Composable
|
|
23
23
|
fun HeaderDefault(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
animations: HeaderAnimations
|
|
24
|
+
opacityAni: Float,
|
|
25
|
+
statusBarHeight: Dp,
|
|
27
26
|
) {
|
|
27
|
+
val height = statusBarHeight + 52.dp
|
|
28
28
|
Box(
|
|
29
29
|
modifier = Modifier.fillMaxWidth().height(height)
|
|
30
|
-
.background(
|
|
30
|
+
.background(AppTheme.current.colors.background.surface)
|
|
31
31
|
.bottomBorder(
|
|
32
32
|
strokeWidth = 1.dp,
|
|
33
33
|
color = AppTheme.current.colors.border.default
|
|
@@ -37,7 +37,7 @@ fun HeaderDefault(
|
|
|
37
37
|
modifier = Modifier
|
|
38
38
|
.height(height)
|
|
39
39
|
.fillMaxWidth()
|
|
40
|
-
.alpha(
|
|
40
|
+
.alpha(opacityAni)
|
|
41
41
|
.background(
|
|
42
42
|
Brush.linearGradient(
|
|
43
43
|
colors = listOf(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package vn.momo.kits.application
|
|
2
2
|
|
|
3
|
+
import androidx.compose.animation.animateColorAsState
|
|
4
|
+
import androidx.compose.animation.core.animateFloatAsState
|
|
3
5
|
import androidx.compose.foundation.background
|
|
4
6
|
import androidx.compose.foundation.layout.Box
|
|
5
7
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
@@ -9,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|
|
9
11
|
import androidx.compose.foundation.layout.width
|
|
10
12
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
11
13
|
import androidx.compose.runtime.Composable
|
|
14
|
+
import androidx.compose.runtime.getValue
|
|
12
15
|
import androidx.compose.ui.Alignment
|
|
13
16
|
import androidx.compose.ui.Modifier
|
|
14
17
|
import androidx.compose.ui.draw.alpha
|
|
@@ -17,86 +20,71 @@ import androidx.compose.ui.graphics.Brush
|
|
|
17
20
|
import androidx.compose.ui.graphics.Color
|
|
18
21
|
import androidx.compose.ui.layout.ContentScale
|
|
19
22
|
import androidx.compose.ui.platform.LocalDensity
|
|
23
|
+
import androidx.compose.ui.unit.Dp
|
|
20
24
|
import androidx.compose.ui.unit.dp
|
|
25
|
+
import androidx.compose.ui.zIndex
|
|
21
26
|
import vn.momo.kits.components.Image
|
|
22
27
|
import vn.momo.kits.components.InputSearch
|
|
23
28
|
import vn.momo.kits.components.InputSearchProps
|
|
24
29
|
import vn.momo.kits.components.Options
|
|
25
30
|
import vn.momo.kits.const.AppTheme
|
|
31
|
+
import vn.momo.kits.const.Colors
|
|
26
32
|
import vn.momo.kits.const.Radius
|
|
27
33
|
import vn.momo.kits.const.Spacing
|
|
34
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
35
|
+
import vn.momo.kits.utils.bottomBorder
|
|
28
36
|
|
|
29
37
|
@Composable
|
|
30
38
|
fun HeaderExtended(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
){
|
|
39
|
+
opacityAni: Float,
|
|
40
|
+
statusBarHeight: Dp,
|
|
41
|
+
backgroundColor: Color
|
|
42
|
+
) {
|
|
36
43
|
Box(
|
|
37
|
-
modifier = Modifier
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
),
|
|
50
|
-
start = Offset(0f, 0f),
|
|
51
|
-
end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
|
|
52
|
-
)
|
|
44
|
+
modifier = Modifier
|
|
45
|
+
.fillMaxWidth()
|
|
46
|
+
.height(154.dp)
|
|
47
|
+
.alpha(opacityAni)
|
|
48
|
+
.background(
|
|
49
|
+
Brush.linearGradient(
|
|
50
|
+
colors = listOf(
|
|
51
|
+
Color(0xFFFDCADE),
|
|
52
|
+
Color(0x00FDCADE)
|
|
53
|
+
),
|
|
54
|
+
start = Offset(0f, 0f),
|
|
55
|
+
end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
|
|
53
56
|
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
modifier = Modifier
|
|
58
|
-
.fillMaxWidth()
|
|
59
|
-
.background(backgroundColor)
|
|
60
|
-
) {
|
|
61
|
-
Image(
|
|
62
|
-
loading = false,
|
|
63
|
-
source = AppTheme.current.assets.headerBackground!!,
|
|
64
|
-
modifier = Modifier.fillMaxWidth().height(154.dp),
|
|
65
|
-
options = Options(
|
|
66
|
-
contentScale = ContentScale.FillWidth
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (inputSearchProps != null && useAnimationSearch) {
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
if (AppTheme.current.assets.headerBackground != null) {
|
|
74
60
|
Box(
|
|
75
61
|
modifier = Modifier
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
x = animations.translateX.dp
|
|
79
|
-
)
|
|
80
|
-
.padding(vertical = Spacing.S)
|
|
81
|
-
.align(Alignment.BottomStart)
|
|
62
|
+
.fillMaxWidth()
|
|
63
|
+
.background(backgroundColor)
|
|
82
64
|
) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
) {
|
|
91
|
-
InputSearch(
|
|
92
|
-
inputSearchProps = inputSearchProps.copy(
|
|
93
|
-
showButtonText = false,
|
|
94
|
-
backgroundColor = Color.Transparent
|
|
95
|
-
)
|
|
65
|
+
Image(
|
|
66
|
+
loading = false,
|
|
67
|
+
source = AppTheme.current.assets.headerBackground!!,
|
|
68
|
+
modifier = Modifier.fillMaxWidth().height(154.dp),
|
|
69
|
+
options = Options(
|
|
70
|
+
contentScale = ContentScale.FillWidth
|
|
96
71
|
)
|
|
97
|
-
|
|
72
|
+
)
|
|
98
73
|
}
|
|
99
74
|
}
|
|
100
75
|
}
|
|
101
|
-
}
|
|
102
76
|
|
|
77
|
+
Box(
|
|
78
|
+
modifier = Modifier
|
|
79
|
+
.fillMaxWidth()
|
|
80
|
+
.alpha(1f - opacityAni)
|
|
81
|
+
.height(statusBarHeight + 52.dp)
|
|
82
|
+
.background(
|
|
83
|
+
AppTheme.current.colors.background.surface
|
|
84
|
+
)
|
|
85
|
+
.bottomBorder(
|
|
86
|
+
strokeWidth = 1.dp,
|
|
87
|
+
color = AppTheme.current.colors.border.default
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -79,12 +79,12 @@ object Spacing {
|
|
|
79
79
|
|
|
80
80
|
@Composable
|
|
81
81
|
fun HeaderRight(
|
|
82
|
-
|
|
82
|
+
headerRight: HeaderRightData? = null,
|
|
83
83
|
) {
|
|
84
84
|
Row(
|
|
85
85
|
verticalAlignment = Alignment.CenterVertically,
|
|
86
86
|
) {
|
|
87
|
-
ToolkitHeaderRight(headerRight =
|
|
87
|
+
ToolkitHeaderRight(headerRight = headerRight)
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -9,7 +9,6 @@ import androidx.compose.runtime.remember
|
|
|
9
9
|
import androidx.compose.runtime.setValue
|
|
10
10
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
11
11
|
import androidx.compose.ui.unit.Dp
|
|
12
|
-
import androidx.compose.ui.unit.dp
|
|
13
12
|
import vn.momo.kits.components.TrustBannerData
|
|
14
13
|
import vn.momo.kits.const.AppStatusBar
|
|
15
14
|
import vn.momo.kits.const.AppTheme
|
|
@@ -75,7 +74,7 @@ data class KitConfig(
|
|
|
75
74
|
fun ApplicationContainer(
|
|
76
75
|
theme: Theme = defaultTheme,
|
|
77
76
|
composeApi: ComposeApi? = null,
|
|
78
|
-
statusBarHeight: Dp? =
|
|
77
|
+
statusBarHeight: Dp? = AppStatusBar.current,
|
|
79
78
|
applicationContext: MiniAppContext? = null,
|
|
80
79
|
config: KitConfig? = null,
|
|
81
80
|
content: @Composable () -> Unit,
|
|
@@ -99,8 +98,6 @@ fun ApplicationContainer(
|
|
|
99
98
|
|
|
100
99
|
val appStatusBarHeight = statusBarHeight ?: getStatusBarHeight()
|
|
101
100
|
|
|
102
|
-
println("appStatusBarHeight ${appStatusBarHeight.value}")
|
|
103
|
-
|
|
104
101
|
disableOverscroll()
|
|
105
102
|
CompositionLocalProvider(
|
|
106
103
|
AppTheme provides appTheme,
|
|
@@ -50,7 +50,6 @@ enum class HeaderType {
|
|
|
50
50
|
@Composable
|
|
51
51
|
fun Screen(
|
|
52
52
|
backgroundColor: Color? = null,
|
|
53
|
-
isBack: Boolean = true,
|
|
54
53
|
headerType: HeaderType = HeaderType.DEFAULT,
|
|
55
54
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
|
56
55
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
|
@@ -64,10 +63,9 @@ fun Screen(
|
|
|
64
63
|
footer: @Composable (() -> Unit)? = null,
|
|
65
64
|
headerRight: @Composable (() -> Unit)? = { HeaderRight() },
|
|
66
65
|
fabProps: FabProps? = null,
|
|
67
|
-
animatedHeader: AnimatedHeader? = null,
|
|
68
|
-
layoutOffset: Dp = 56.dp,
|
|
69
66
|
inputSearchProps: InputSearchProps? = null,
|
|
70
67
|
useAnimationSearch: Boolean = false,
|
|
68
|
+
headerRightWidth: Dp = 0.dp,
|
|
71
69
|
content: @Composable () -> Unit,
|
|
72
70
|
) {
|
|
73
71
|
val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
|
|
@@ -84,58 +82,34 @@ fun Screen(
|
|
|
84
82
|
val bottomPadding = min(indicator, 21.dp)
|
|
85
83
|
|
|
86
84
|
val opacity by animateFloatAsState(
|
|
87
|
-
targetValue = ((scrollState.value /
|
|
85
|
+
targetValue = ((scrollState.value / 52f)).coerceIn(0f, 1f),
|
|
88
86
|
)
|
|
89
87
|
|
|
90
|
-
val headerAnimated =
|
|
91
|
-
@Composable {
|
|
92
|
-
animatedHeader?.composable?.invoke(scrollState.value)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
88
|
Box(
|
|
96
89
|
Modifier.fillMaxSize()
|
|
97
|
-
.background(AppTheme.current.colors.background.
|
|
90
|
+
.background(backgroundColor ?: AppTheme.current.colors.background.default)
|
|
98
91
|
) {
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
inputSearchProps = inputSearchProps,
|
|
105
|
-
useAnimationSearch = useAnimationSearch
|
|
106
|
-
)
|
|
107
|
-
} else {
|
|
108
|
-
Box(
|
|
109
|
-
Modifier.zIndex(2f)
|
|
110
|
-
.background(Color.White.copy(alpha = opacity))
|
|
111
|
-
.graphicsLayer { alpha = opacity }
|
|
112
|
-
) {
|
|
113
|
-
HeaderBackground(
|
|
114
|
-
isBack = isBack,
|
|
115
|
-
headerType = headerType,
|
|
116
|
-
scrollState = scrollState.value,
|
|
117
|
-
inputSearchProps = inputSearchProps,
|
|
118
|
-
useAnimationSearch = useAnimationSearch
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
93
|
+
HeaderBackground(
|
|
94
|
+
headerType = headerType,
|
|
95
|
+
scrollState = scrollState.value,
|
|
96
|
+
)
|
|
123
97
|
Header(
|
|
124
98
|
headerType = headerType,
|
|
125
99
|
title = title,
|
|
126
100
|
titlePosition = titlePosition,
|
|
127
101
|
headerRight = headerRight,
|
|
102
|
+
headerRightWidth = headerRightWidth,
|
|
128
103
|
goBack = goBack,
|
|
129
104
|
opacity = opacity,
|
|
130
|
-
animatedHeader = animatedHeader,
|
|
131
105
|
inputSearchProps = inputSearchProps,
|
|
132
106
|
scrollState = scrollState.value,
|
|
133
|
-
useAnimationSearch = useAnimationSearch
|
|
107
|
+
useAnimationSearch = useAnimationSearch,
|
|
134
108
|
)
|
|
135
109
|
|
|
136
110
|
Column(
|
|
137
111
|
modifier = Modifier.fillMaxSize()
|
|
138
|
-
.padding(top = if (
|
|
112
|
+
.padding(top = if (headerType !== HeaderType.NONE) statusBarHeight + 52.dp else 0.dp)
|
|
139
113
|
.pointerInput(Unit) {
|
|
140
114
|
detectTapGestures(onTap = {
|
|
141
115
|
keyboardController?.hide()
|
|
@@ -143,7 +117,6 @@ fun Screen(
|
|
|
143
117
|
}
|
|
144
118
|
.zIndex(1f),
|
|
145
119
|
) {
|
|
146
|
-
|
|
147
120
|
Column(
|
|
148
121
|
modifier = Modifier
|
|
149
122
|
.conditional(scrollable) { weight(1f) }
|
|
@@ -155,18 +128,9 @@ fun Screen(
|
|
|
155
128
|
verticalArrangement = verticalArrangement,
|
|
156
129
|
horizontalAlignment = horizontalAlignment
|
|
157
130
|
) {
|
|
158
|
-
Box
|
|
159
|
-
modifier = Modifier.background(
|
|
160
|
-
backgroundColor ?: AppTheme.current.colors.background.default
|
|
161
|
-
)
|
|
162
|
-
) {
|
|
163
|
-
headerAnimated()
|
|
131
|
+
Box {
|
|
164
132
|
Column {
|
|
165
|
-
if (
|
|
166
|
-
Spacer(modifier = Modifier.padding(top = statusBarHeight + 52.dp + layoutOffset))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (useAnimationSearch && inputSearchProps != null) {
|
|
133
|
+
if (useAnimationSearch && inputSearchProps != null && headerType == HeaderType.EXTENDED) {
|
|
170
134
|
Spacer(modifier = Modifier.padding(top = statusBarHeight))
|
|
171
135
|
}
|
|
172
136
|
content()
|
|
@@ -3,59 +3,63 @@ package vn.momo.kits.application
|
|
|
3
3
|
import androidx.compose.animation.animateColorAsState
|
|
4
4
|
import androidx.compose.animation.core.animateFloatAsState
|
|
5
5
|
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.getValue
|
|
6
7
|
import androidx.compose.ui.graphics.Color
|
|
7
8
|
import androidx.compose.ui.unit.Dp
|
|
9
|
+
import androidx.compose.ui.unit.dp
|
|
8
10
|
import vn.momo.kits.const.AppTheme
|
|
9
11
|
import vn.momo.kits.const.Colors
|
|
10
|
-
import
|
|
12
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
11
13
|
|
|
12
14
|
data class HeaderAnimations(
|
|
13
15
|
val opacity: Float,
|
|
14
16
|
val backgroundSearch: Color,
|
|
15
17
|
val translateX: Float,
|
|
16
18
|
val width: Float,
|
|
17
|
-
val
|
|
19
|
+
val translateY: Float
|
|
18
20
|
)
|
|
19
21
|
|
|
22
|
+
private const val SCREEN_PADDING = 12
|
|
23
|
+
private const val BACK_WIDTH = 28
|
|
24
|
+
|
|
20
25
|
@Composable
|
|
21
26
|
fun useHeaderSearchAnimation(
|
|
27
|
+
opacityAni: Float,
|
|
22
28
|
scrollState: Int,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
searchWidth: Int,
|
|
26
|
-
headerRightWidth: Dp
|
|
29
|
+
headerRightWidth: Dp,
|
|
30
|
+
isBack: Boolean
|
|
27
31
|
): HeaderAnimations {
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
)
|
|
32
|
+
val screenWidth = getScreenDimensions().width
|
|
33
|
+
val leftPosition = if (isBack) (BACK_WIDTH + 20).dp else 12.dp
|
|
34
|
+
val searchWidth = screenWidth - (SCREEN_PADDING * 2)
|
|
31
35
|
|
|
32
36
|
val backgroundSearch by animateColorAsState(
|
|
33
37
|
targetValue = animateColor(
|
|
34
|
-
AppTheme.current.colors.background.default,
|
|
35
38
|
Colors.black_01,
|
|
39
|
+
AppTheme.current.colors.background.default,
|
|
36
40
|
opacityAni
|
|
37
|
-
)
|
|
41
|
+
),
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
val animatedTranslateX by animateFloatAsState(
|
|
41
|
-
targetValue = (scrollState / 52f).coerceIn(0f, 1f) * (leftPosition.value - 12) + 12
|
|
45
|
+
targetValue = (scrollState / 52f).coerceIn(0f, 1f) * (leftPosition.value - 12) + 12,
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
val animatedWidth by animateFloatAsState(
|
|
45
|
-
targetValue = (scrollState / 52f).coerceIn(
|
|
46
|
-
|
|
49
|
+
targetValue = (scrollState / 52f).coerceIn(
|
|
50
|
+
0f,
|
|
51
|
+
1f
|
|
52
|
+
) * ((searchWidth - leftPosition.value - headerRightWidth.value + 12) - searchWidth) + searchWidth,
|
|
47
53
|
)
|
|
48
54
|
|
|
49
|
-
val
|
|
50
|
-
targetValue = (1 - (scrollState / 52f).coerceIn(0f, 1f)) *
|
|
51
|
-
(statusBarHeight.value + 94 - (statusBarHeight.value + 52)) + (statusBarHeight.value + 52)
|
|
55
|
+
val animatedTranslateY by animateFloatAsState(
|
|
56
|
+
targetValue = (1 - (scrollState / 52f).coerceIn(0f, 1f)) * 52
|
|
52
57
|
)
|
|
53
|
-
|
|
54
58
|
return HeaderAnimations(
|
|
55
59
|
opacity = opacityAni,
|
|
56
60
|
backgroundSearch = backgroundSearch,
|
|
57
61
|
translateX = animatedTranslateX,
|
|
58
62
|
width = animatedWidth,
|
|
59
|
-
|
|
63
|
+
translateY = animatedTranslateY
|
|
60
64
|
)
|
|
61
65
|
}
|
|
@@ -35,6 +35,7 @@ import vn.momo.kits.const.AppTheme
|
|
|
35
35
|
import vn.momo.kits.const.Radius
|
|
36
36
|
import vn.momo.kits.const.Spacing
|
|
37
37
|
import vn.momo.kits.const.Typography
|
|
38
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
38
39
|
import vn.momo.kits.utils.ifNotNull
|
|
39
40
|
|
|
40
41
|
data class PopupAction(
|
|
@@ -95,12 +96,12 @@ fun PopupNotify(
|
|
|
95
96
|
callback?.invoke()
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
Box(Modifier.
|
|
99
|
-
|
|
100
|
-
}, contentAlignment = Alignment.TopEnd) {
|
|
99
|
+
Box(Modifier.setAutomationId("popup_notify")
|
|
100
|
+
, contentAlignment = Alignment.TopEnd) {
|
|
101
101
|
Column(
|
|
102
102
|
modifier = Modifier
|
|
103
103
|
.fillMaxWidth()
|
|
104
|
+
.padding(horizontal = 12.dp)
|
|
104
105
|
.clip(RoundedCornerShape(Radius.L))
|
|
105
106
|
.background(
|
|
106
107
|
AppTheme.current.colors.background.surface,
|
|
@@ -115,7 +116,7 @@ fun PopupNotify(
|
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
Column(modifier = Modifier.padding(Spacing.XL)) {
|
|
118
|
-
Text(style = Typography.titleXs, maxLines = 2, text = title)
|
|
119
|
+
Text(style = Typography.titleXs, maxLines = 2, text = title, modifier = Modifier.setAutomationId("title_popup_permission"))
|
|
119
120
|
content(Modifier.padding(top = Spacing.S))
|
|
120
121
|
if (error.isNotEmpty()) {
|
|
121
122
|
Text(
|
|
@@ -142,7 +143,7 @@ fun PopupNotify(
|
|
|
142
143
|
Modifier
|
|
143
144
|
.width(22.dp)
|
|
144
145
|
.height(22.dp)
|
|
145
|
-
.offset(x = Spacing.
|
|
146
|
+
.offset(x = Spacing.M / 2, y = -Spacing.M)
|
|
146
147
|
.background(
|
|
147
148
|
color = AppTheme.current.colors.text.default,
|
|
148
149
|
shape = RoundedCornerShape(Radius.M)
|
|
@@ -164,9 +165,7 @@ fun PopupNotify(
|
|
|
164
165
|
source = "navigation_close",
|
|
165
166
|
color = AppTheme.current.colors.background.surface,
|
|
166
167
|
size = 16.dp,
|
|
167
|
-
modifier = Modifier.
|
|
168
|
-
testTag = "ic_popup_close"
|
|
169
|
-
}
|
|
168
|
+
modifier = Modifier.setAutomationId("ic_popup_close")
|
|
170
169
|
)
|
|
171
170
|
}
|
|
172
171
|
}
|
|
@@ -233,9 +232,7 @@ fun renderRow(
|
|
|
233
232
|
},
|
|
234
233
|
title = it.title,
|
|
235
234
|
type = ButtonType.TEXT,
|
|
236
|
-
modifier = Modifier.
|
|
237
|
-
testTag = "btn_popup_cancel"
|
|
238
|
-
}
|
|
235
|
+
modifier = Modifier.setAutomationId("btn_popup_cancel")
|
|
239
236
|
)
|
|
240
237
|
}
|
|
241
238
|
Spacer(modifier = Modifier.width(Spacing.S))
|
|
@@ -248,9 +245,7 @@ fun renderRow(
|
|
|
248
245
|
onAction(primary?.onPress)
|
|
249
246
|
},
|
|
250
247
|
title = primary?.title ?: "",
|
|
251
|
-
modifier = Modifier.
|
|
252
|
-
testTag = "btn_popup_allow"
|
|
253
|
-
}
|
|
248
|
+
modifier = Modifier.setAutomationId("btn_popup_allow")
|
|
254
249
|
)
|
|
255
250
|
}
|
|
256
251
|
}
|
|
@@ -271,9 +266,7 @@ fun renderColumn(
|
|
|
271
266
|
onAction(primary?.onPress)
|
|
272
267
|
},
|
|
273
268
|
title = primary?.title ?: "",
|
|
274
|
-
modifier = Modifier.
|
|
275
|
-
testTag = "btn_popup_allow"
|
|
276
|
-
}
|
|
269
|
+
modifier = Modifier.setAutomationId("btn_popup_allow")
|
|
277
270
|
)
|
|
278
271
|
secondary?.let {
|
|
279
272
|
Spacer(modifier = Modifier.height(Spacing.S))
|
|
@@ -286,9 +279,7 @@ fun renderColumn(
|
|
|
286
279
|
},
|
|
287
280
|
title = it.title,
|
|
288
281
|
type = ButtonType.TEXT,
|
|
289
|
-
modifier = Modifier.
|
|
290
|
-
testTag = "btn_popup_cancel"
|
|
291
|
-
}
|
|
282
|
+
modifier = Modifier.setAutomationId("btn_popup_cancel")
|
|
292
283
|
)
|
|
293
284
|
}
|
|
294
285
|
}
|
|
@@ -37,7 +37,7 @@ fun Text(
|
|
|
37
37
|
modifier = modifier.setAutomationId(accessibilityId?:text, text),
|
|
38
38
|
text = text,
|
|
39
39
|
color = color ?: AppTheme.current.colors.text.default,
|
|
40
|
-
style = style,
|
|
40
|
+
style = style.copy(fontWeight = null),
|
|
41
41
|
textAlign = textAlign,
|
|
42
42
|
fontSize = fontSize,
|
|
43
43
|
fontFamily = font,
|
package/ios/Image/Image.swift
CHANGED
|
@@ -46,8 +46,10 @@ public struct PopupDisplay: View {
|
|
|
46
46
|
.overlay(RoundedRectangle(cornerRadius: Radius.L).stroke(Colors.black01, lineWidth: 1))
|
|
47
47
|
}.foregroundColor(Colors.black20)
|
|
48
48
|
.background(Colors.black01.cornerRadius(15))
|
|
49
|
-
.
|
|
49
|
+
.padding(.trailing, -Spacing.M )
|
|
50
|
+
.padding(.top, -Spacing.M)
|
|
50
51
|
.zIndex(1)
|
|
52
|
+
.accessibility(identifier: "ic_popup_close")
|
|
51
53
|
|
|
52
54
|
VStack {
|
|
53
55
|
if(!url.isEmpty) {
|
|
@@ -70,6 +72,7 @@ public struct PopupDisplay: View {
|
|
|
70
72
|
.padding(.top, 16)
|
|
71
73
|
.padding(.bottom, 8)
|
|
72
74
|
.lineLimit(2)
|
|
75
|
+
.accessibility(identifier: "title_popup_permission")
|
|
73
76
|
|
|
74
77
|
Text(description)
|
|
75
78
|
.foregroundColor(.black)
|
|
@@ -77,7 +80,9 @@ public struct PopupDisplay: View {
|
|
|
77
80
|
.opacity(0.6)
|
|
78
81
|
.multilineTextAlignment(.leading)
|
|
79
82
|
.padding(.bottom, 20)
|
|
80
|
-
}
|
|
83
|
+
}
|
|
84
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
85
|
+
.padding(.horizontal, 24)
|
|
81
86
|
|
|
82
87
|
if buttonDirection == .row {
|
|
83
88
|
if buttonType == .text {
|
|
@@ -97,7 +102,7 @@ public struct PopupDisplay: View {
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
.background(Color.white.cornerRadius(15))
|
|
100
|
-
.padding(.horizontal,
|
|
105
|
+
.padding(.horizontal, 12)
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
// MARK: Internal
|
|
@@ -150,8 +155,9 @@ public struct PopupDisplay: View {
|
|
|
150
155
|
|
|
151
156
|
var RowButtons: some View {
|
|
152
157
|
HStack {
|
|
153
|
-
Button(title: closeButtonTitle, action: onPressCloseButton, type: .secondary, size: .medium)
|
|
158
|
+
Button(title: closeButtonTitle, action: onPressCloseButton, type: .secondary, size: .medium).accessibility(identifier: "btn_popup_cancel")
|
|
154
159
|
Button(title: actionButtonTitle, action: onPressAction, size: .medium)
|
|
160
|
+
.accessibility(identifier: "btn_popup_allow")
|
|
155
161
|
}
|
|
156
162
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
157
163
|
.padding(.horizontal, 24)
|
|
@@ -164,8 +170,9 @@ public struct PopupDisplay: View {
|
|
|
164
170
|
title: actionButtonTitle,
|
|
165
171
|
action: onPressAction,
|
|
166
172
|
size: .medium
|
|
167
|
-
)
|
|
168
|
-
Button(title: closeButtonTitle, action: onPressCloseButton, type: .secondary, size: .medium)
|
|
173
|
+
).accessibility(identifier: "btn_popup_allow")
|
|
174
|
+
Button(title: closeButtonTitle, action: onPressCloseButton, type: .secondary, size: .medium).accessibility(identifier: "btn_popup_cancel")
|
|
175
|
+
|
|
169
176
|
}
|
|
170
177
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
171
178
|
.padding(.horizontal, 24)
|